cntx-ui 3.0.4 → 3.0.6

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/bin/cntx-ui.js CHANGED
@@ -3,19 +3,19 @@
3
3
  import { readFileSync } from 'fs';
4
4
  import { dirname, join } from 'path';
5
5
  import { fileURLToPath } from 'url';
6
- import { startServer, startMCPServer, generateBundle, initConfig, getStatus, setupMCP } from '../server.js';
6
+ import { autoInitAndStart, startMCPServer, generateBundle, initConfig, getStatus } from '../server.js';
7
7
 
8
8
  const __dirname = dirname(fileURLToPath(import.meta.url));
9
9
  const packagePath = join(__dirname, '..', 'package.json');
10
10
  const packageJson = JSON.parse(readFileSync(packagePath, 'utf8'));
11
11
 
12
12
  const args = process.argv.slice(2);
13
- const command = args[0] || 'help';
13
+ const command = args[0] || 'start';
14
14
  const isVerbose = args.includes('--verbose');
15
15
 
16
16
  // Graceful shutdown
17
17
  process.on('SIGINT', () => {
18
- console.log('\n👋 Shutting down cntx-ui...');
18
+ console.log('\n Shutting down cntx-ui...');
19
19
  process.exit(0);
20
20
  });
21
21
 
@@ -25,55 +25,30 @@ function showHelp() {
25
25
  ${packageJson.description}
26
26
 
27
27
  Usage:
28
- cntx-ui <command> [options]
28
+ cntx-ui [command] [options]
29
29
 
30
30
  Commands:
31
+ (default) Auto-init if needed, then start web server
31
32
  init Initialize configuration in current directory
32
- watch [port] Start web server (default port: 3333)
33
33
  mcp Start MCP server (stdio transport)
34
34
  bundle [name] Generate specific bundle (default: master)
35
35
  status Show current project status
36
- setup-mcp Add this project to Claude Desktop MCP config
37
36
 
38
37
  Options:
39
38
  --verbose Enable detailed logging
40
- --with-mcp Start web server with MCP status tracking
41
39
  --version, -v Show version number
42
40
  --help, -h Show this help message
43
41
 
44
42
  Examples:
43
+ cntx-ui Start server (auto-inits if needed)
45
44
  cntx-ui init Initialize a new project
46
- cntx-ui watch Start web server on port 3333
47
- cntx-ui watch 8080 Start web server on port 8080
48
- cntx-ui watch --verbose Start with detailed logs
49
- cntx-ui watch --with-mcp Start with MCP integration
45
+ cntx-ui mcp Start MCP server on stdio
50
46
  cntx-ui bundle api Generate 'api' bundle
51
47
  cntx-ui status Show project status
52
- cntx-ui setup-mcp Configure Claude Desktop integration
53
48
 
54
49
  MCP Integration:
55
- The MCP server provides AI-accessible bundle management for Claude Desktop
56
- and other MCP-compatible clients. Use 'setup-mcp' to configure automatic
57
- integration with Claude Desktop.
58
-
59
- Agent Collaboration:
60
- To get an external agent up to speed with your project, use this prompt:
61
-
62
- "I'm working in a project that uses cntx-ui for file organization and AI
63
- collaboration. Please read these files to understand the project structure
64
- and help me with activities:
65
-
66
- @.cntx/agent-instructions.md
67
- @.cntx/activities/README.md
68
- @.cntx/activities/activities.json
69
-
70
- After reading those, please also examine:
71
- @.cntx/activities/lib/create-activity.mdc
72
- @.cntx/activities/lib/generate-tasks.mdc
73
- @.cntx/activities/lib/process-task-list.mdc
74
-
75
- These files contain the complete workflow for creating and managing
76
- activities with agent assistance."
50
+ Running 'cntx-ui init' creates a .mcp.json file so Claude Code
51
+ can auto-discover the MCP server. Run 'cntx-ui mcp' for stdio mode.
77
52
 
78
53
  Repository: ${packageJson.repository.url}
79
54
  Author: ${packageJson.author}
@@ -95,17 +70,13 @@ async function main() {
95
70
  return showHelp();
96
71
  }
97
72
 
98
- // Handle default command (watch if no command provided and no flags)
99
- const actualCommand = command === 'help' ? 'watch' : command;
100
-
101
73
  try {
102
- switch (actualCommand) {
74
+ switch (command) {
75
+ case 'start':
103
76
  case 'watch':
104
77
  case 'w':
105
78
  const port = parseInt(args[1]) || 3333;
106
- // Enable MCP status tracking by default for the web dashboard
107
- const withMcp = !args.includes('--no-mcp');
108
- await startServer({ port, withMcp, verbose: isVerbose });
79
+ await autoInitAndStart({ port, verbose: isVerbose });
109
80
  break;
110
81
 
111
82
  case 'mcp':
@@ -118,9 +89,9 @@ async function main() {
118
89
  const bundleName = args[1] || 'master';
119
90
  try {
120
91
  await generateBundle(bundleName);
121
- console.log(`✅ Bundle '${bundleName}' generated successfully`);
92
+ console.log(`Bundle '${bundleName}' generated successfully`);
122
93
  } catch (error) {
123
- console.error(`❌ Failed to generate bundle '${bundleName}': ${error.message}`);
94
+ console.error(`Failed to generate bundle '${bundleName}': ${error.message}`);
124
95
  process.exit(1);
125
96
  }
126
97
  break;
@@ -135,17 +106,13 @@ async function main() {
135
106
  await getStatus();
136
107
  break;
137
108
 
138
- case 'setup-mcp':
139
- setupMCP();
140
- break;
141
-
142
109
  default:
143
- console.error(`❌ Unknown command: ${command}`);
110
+ console.error(`Unknown command: ${command}`);
144
111
  console.log('Run "cntx-ui --help" for usage information.');
145
112
  process.exit(1);
146
113
  }
147
114
  } catch (error) {
148
- console.error(`❌ Error: ${error.message}`);
115
+ console.error(`Error: ${error.message}`);
149
116
  if (isVerbose) {
150
117
  console.error(error.stack);
151
118
  }
@@ -281,6 +281,76 @@ This agent is **stateful**. All interactions in this directory are logged to a p
281
281
  return { strategy: 'TBD', description: 'Ready to plan' };
282
282
  }
283
283
 
284
+ /**
285
+ * Discussion Mode: Engage in architectural discussion about the codebase
286
+ */
287
+ async discussAndPlan(userInput, context = {}) {
288
+ try {
289
+ await this.logInteraction('user', userInput);
290
+
291
+ const overview = await this.getCodebaseOverview();
292
+ const searchResults = await this.cntxServer.vectorStore.search(userInput, { limit: 5 });
293
+
294
+ const response = {
295
+ input: userInput,
296
+ context,
297
+ overview,
298
+ relevantCode: searchResults.map(r => ({
299
+ name: r.name,
300
+ filePath: r.filePath,
301
+ purpose: r.purpose,
302
+ type: r.type
303
+ })),
304
+ suggestions: [
305
+ 'Review the relevant code sections listed above.',
306
+ 'Consider how changes would affect dependent bundles.',
307
+ 'Use semantic search to explore related patterns.'
308
+ ]
309
+ };
310
+
311
+ await this.logInteraction('agent', `Discussion: ${userInput}`, { response });
312
+
313
+ return response;
314
+ } catch (error) {
315
+ throw new Error(`Discussion failed: ${error.message}`);
316
+ }
317
+ }
318
+
319
+ /**
320
+ * Organize Mode: Setup and maintenance of project organization
321
+ */
322
+ async organizeProject(options = {}) {
323
+ const { activity = 'detect', autoDetect = true, force = false } = options;
324
+
325
+ try {
326
+ await this.logInteraction('agent', `Organizing project: ${activity}`);
327
+
328
+ const overview = await this.getCodebaseOverview();
329
+ const bundles = await this.analyzeBundles('all');
330
+
331
+ const response = {
332
+ activity,
333
+ overview,
334
+ bundles: bundles.map(b => ({
335
+ name: b.name,
336
+ fileCount: b.fileCount,
337
+ purpose: b.purpose
338
+ })),
339
+ recommendations: [
340
+ 'Review bundle organization for coverage gaps.',
341
+ 'Check .cntxignore for missing exclusion patterns.',
342
+ 'Run semantic analysis to identify uncategorized code.'
343
+ ]
344
+ };
345
+
346
+ await this.logInteraction('agent', `Organization complete for activity: ${activity}`, { response });
347
+
348
+ return response;
349
+ } catch (error) {
350
+ throw new Error(`Organization failed: ${error.message}`);
351
+ }
352
+ }
353
+
284
354
  async generateContextualAnswer(question, results, includeCode) {
285
355
  let response = `Based on the codebase analysis:\n\n`;
286
356
  if (results.chunks.length > 0) {
package/lib/api-router.js CHANGED
@@ -8,26 +8,19 @@ import fs from 'fs';
8
8
  import path from 'path';
9
9
 
10
10
  export default class APIRouter {
11
- constructor(cntxServer, configManager, bundleManager, fileSystemManager, semanticAnalysisManager, vectorStore, activityManager) {
11
+ constructor(cntxServer, configManager, bundleManager, fileSystemManager, semanticAnalysisManager, vectorStore) {
12
12
  this.cntxServer = cntxServer;
13
13
  this.configManager = configManager;
14
14
  this.bundleManager = bundleManager;
15
15
  this.fileSystemManager = fileSystemManager;
16
16
  this.semanticAnalysisManager = semanticAnalysisManager;
17
17
  this.vectorStore = vectorStore;
18
- this.activityManager = activityManager;
19
18
  }
20
19
 
21
20
  async handleRequest(req, res, url) {
22
21
  const method = req.method;
23
22
  const pathname = url.pathname;
24
23
 
25
- // DEBUG: Log all incoming API requests
26
- console.log('[API REQUEST]', method, pathname);
27
- if (pathname.includes('database')) {
28
- console.log('[DATABASE] Route requested:', pathname, method);
29
- }
30
-
31
24
  try {
32
25
  // Route to appropriate handler
33
26
  if (pathname === '/api/bundles' && method === 'GET') {
@@ -73,26 +66,6 @@ export default class APIRouter {
73
66
  return await this.handleGetFiles(req, res);
74
67
  }
75
68
 
76
- if (pathname === '/api/cursor-rules' && method === 'GET') {
77
- return await this.handleGetCursorRules(req, res);
78
- }
79
-
80
- if (pathname === '/api/cursor-rules' && method === 'POST') {
81
- return await this.handlePostCursorRules(req, res);
82
- }
83
-
84
- if (pathname === '/api/cursor-rules/templates' && method === 'GET') {
85
- return await this.handleGetCursorRulesTemplates(req, res);
86
- }
87
-
88
- if (pathname === '/api/claude-md' && method === 'GET') {
89
- return await this.handleGetClaudeMd(req, res);
90
- }
91
-
92
- if (pathname === '/api/claude-md' && method === 'POST') {
93
- return await this.handlePostClaudeMd(req, res);
94
- }
95
-
96
69
  if (pathname === '/api/heuristics/config' && method === 'GET') {
97
70
  return await this.handleGetHeuristicsConfig(req, res);
98
71
  }
@@ -190,25 +163,6 @@ export default class APIRouter {
190
163
  return await this.handlePostVectorDbSearchByDomain(req, res);
191
164
  }
192
165
 
193
- if (pathname === '/api/activities' && method === 'GET') {
194
- return await this.handleGetActivities(req, res);
195
- }
196
-
197
- if (pathname.startsWith('/api/activities/') && pathname.endsWith('/reasoning') && method === 'GET') {
198
- const activityId = pathname.split('/')[3];
199
- return await this.handleGetActivityReasoning(req, res, activityId);
200
- }
201
-
202
- if (pathname.startsWith('/api/activities/') && pathname.endsWith('/execute') && method === 'POST') {
203
- const activityId = pathname.split('/')[3];
204
- return await this.handlePostActivityExecute(req, res, activityId);
205
- }
206
-
207
- if (pathname.startsWith('/api/activities/') && pathname.endsWith('/stop') && method === 'POST') {
208
- const activityId = pathname.split('/')[3];
209
- return await this.handlePostActivityStop(req, res, activityId);
210
- }
211
-
212
166
  if (pathname === '/api/open-file' && method === 'POST') {
213
167
  return await this.handleOpenFile(req, res);
214
168
  }
@@ -429,49 +383,6 @@ export default class APIRouter {
429
383
  res.end(JSON.stringify({ success: true }));
430
384
  }
431
385
 
432
- async handleGetCursorRules(req, res) {
433
- const content = this.configManager.loadCursorRules();
434
- res.writeHead(200, { 'Content-Type': 'text/plain' });
435
- res.end(content);
436
- }
437
-
438
- async handlePostCursorRules(req, res) {
439
- const body = await this.getRequestBody(req);
440
- const { content } = JSON.parse(body);
441
-
442
- this.configManager.saveCursorRules(content);
443
- res.writeHead(200, { 'Content-Type': 'application/json' });
444
- res.end(JSON.stringify({ success: true }));
445
- }
446
-
447
- async handleGetCursorRulesTemplates(req, res) {
448
- const templates = {
449
- react: this.configManager.generateCursorRulesTemplate({ projectType: 'react', name: 'React Project' }),
450
- vue: this.configManager.generateCursorRulesTemplate({ projectType: 'vue', name: 'Vue Project' }),
451
- angular: this.configManager.generateCursorRulesTemplate({ projectType: 'angular', name: 'Angular Project' }),
452
- 'node-backend': this.configManager.generateCursorRulesTemplate({ projectType: 'node-backend', name: 'Node.js Backend' }),
453
- javascript: this.configManager.generateCursorRulesTemplate({ projectType: 'javascript', name: 'JavaScript Project' })
454
- };
455
-
456
- res.writeHead(200, { 'Content-Type': 'application/json' });
457
- res.end(JSON.stringify(templates));
458
- }
459
-
460
- async handleGetClaudeMd(req, res) {
461
- const content = this.configManager.loadClaudeMd();
462
- res.writeHead(200, { 'Content-Type': 'text/plain' });
463
- res.end(content);
464
- }
465
-
466
- async handlePostClaudeMd(req, res) {
467
- const body = await this.getRequestBody(req);
468
- const { content } = JSON.parse(body);
469
-
470
- this.configManager.saveClaudeMd(content);
471
- res.writeHead(200, { 'Content-Type': 'application/json' });
472
- res.end(JSON.stringify({ success: true }));
473
- }
474
-
475
386
  async handleGetHeuristicsConfig(req, res) {
476
387
  const config = this.configManager.loadHeuristicsConfig();
477
388
  res.writeHead(200, { 'Content-Type': 'application/json' });
@@ -907,52 +818,6 @@ export default class APIRouter {
907
818
  }
908
819
  }
909
820
 
910
- // === Activities ===
911
-
912
- async handleGetActivities(req, res) {
913
- try {
914
- const activities = await this.activityManager.loadActivities();
915
- res.writeHead(200, { 'Content-Type': 'application/json' });
916
- res.end(JSON.stringify(activities));
917
- } catch (error) {
918
- res.writeHead(500, { 'Content-Type': 'application/json' });
919
- res.end(JSON.stringify({ error: error.message }));
920
- }
921
- }
922
-
923
- async handleGetActivityReasoning(req, res, activityId) {
924
- try {
925
- const history = this.configManager.dbManager.getSessionHistory(activityId);
926
- res.writeHead(200, { 'Content-Type': 'application/json' });
927
- res.end(JSON.stringify({ history }));
928
- } catch (error) {
929
- res.writeHead(500, { 'Content-Type': 'application/json' });
930
- res.end(JSON.stringify({ error: error.message }));
931
- }
932
- }
933
-
934
- async handlePostActivityExecute(req, res, activityId) {
935
- try {
936
- const result = await this.activityManager.executeActivity(activityId);
937
- res.writeHead(200, { 'Content-Type': 'application/json' });
938
- res.end(JSON.stringify(result));
939
- } catch (error) {
940
- res.writeHead(500, { 'Content-Type': 'application/json' });
941
- res.end(JSON.stringify({ error: error.message }));
942
- }
943
- }
944
-
945
- async handlePostActivityStop(req, res, activityId) {
946
- try {
947
- const result = await this.activityManager.stopActivity(activityId);
948
- res.writeHead(200, { 'Content-Type': 'application/json' });
949
- res.end(JSON.stringify(result));
950
- } catch (error) {
951
- res.writeHead(500, { 'Content-Type': 'application/json' });
952
- res.end(JSON.stringify({ error: error.message }));
953
- }
954
- }
955
-
956
821
  async handleOpenFile(req, res) {
957
822
  try {
958
823
  const body = await this.getRequestBody(req);
@@ -51,38 +51,87 @@ export default class BundleManager {
51
51
  }
52
52
 
53
53
  /**
54
- * Generate Smart Bundle definitions from indexed semantic data
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.
55
57
  */
56
58
  generateSmartBundleDefinitions() {
57
59
  const smartBundles = [];
60
+ const MIN_CHUNKS = 3; // Skip bundles with fewer than this many chunks
61
+
58
62
  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, '-')}`;
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;
64
76
  smartBundles.push({
65
- name,
66
- purpose: row.purpose,
67
- fileCount: row.count,
77
+ name: `smart:${domain}`,
78
+ purpose: domain,
79
+ fileCount: count,
68
80
  type: 'smart',
69
- description: `Automatically grouped by purpose: ${row.purpose}`
81
+ description: `${count} chunks in the ${domain} domain`
70
82
  });
71
- });
83
+ }
72
84
 
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, '-')}`;
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;
78
105
  smartBundles.push({
79
- name,
80
- purpose: row.subtype,
81
- fileCount: row.count,
106
+ name: `smart:dir-${row.dir_group}`,
107
+ purpose: row.dir_group,
108
+ fileCount: row.file_cnt,
82
109
  type: 'smart',
83
- description: `All ${row.subtype} elements across the codebase`
110
+ description: `${row.file_cnt} files in ${row.dir_group} directories`
84
111
  });
85
- });
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
+ }
86
135
  } catch (e) {
87
136
  if (this.verbose) console.warn('Smart bundle discovery failed:', e.message);
88
137
  }
@@ -113,15 +162,56 @@ export default class BundleManager {
113
162
  const query = bundleName.replace('smart:', '');
114
163
  let rows = [];
115
164
 
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);
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);
119
202
  } 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);
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 */ }
124
213
  }
214
+ return Array.from(files);
125
215
  }
126
216
  return rows.map(r => r.file_path);
127
217
  }