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
@@ -102,18 +102,66 @@ export default class HeuristicsManager {
102
102
  * Determine function purpose using configured heuristics
103
103
  */
104
104
  determinePurpose(func) {
105
- const config = this.getConfig()
106
- const name = func.name.toLowerCase()
105
+ const config = this.getConfig();
106
+ const name = func.name.toLowerCase();
107
+ const pathParts = func.pathParts || [];
107
108
 
109
+ // console.log(`🔍 Determining purpose for: ${func.name} in ${pathParts.join('/')}`);
110
+
108
111
  // Check each purpose pattern
109
112
  for (const [patternName, pattern] of Object.entries(config.purposeHeuristics.patterns)) {
110
- if (this.evaluateConditions(pattern.conditions, { func, name })) {
111
- return pattern.purpose
113
+ if (this.evaluateConditions(pattern.conditions, { func, name, pathParts })) {
114
+ // console.log(` ✅ Matched pattern: ${patternName} -> ${pattern.purpose}`);
115
+ return pattern.purpose;
112
116
  }
113
117
  }
114
118
 
115
119
  // Return fallback
116
- return config.purposeHeuristics.fallback.purpose
120
+ return config.purposeHeuristics.fallback.purpose;
121
+ }
122
+
123
+ /**
124
+ * Infer business domains (e.g. auth, editing, file-mgmt)
125
+ */
126
+ inferBusinessDomains(func) {
127
+ const domains = new Set();
128
+ const name = func.name.toLowerCase();
129
+ const path = (func.pathParts || []).join('/').toLowerCase();
130
+ const imports = func.includes?.imports || [];
131
+
132
+ // Path-based domains
133
+ if (path.includes('auth')) domains.add('authentication');
134
+ if (path.includes('component') || path.includes('ui')) domains.add('ui-layer');
135
+ if (path.includes('service') || path.includes('api')) domains.add('api-integration');
136
+ if (path.includes('test') || path.includes('spec')) domains.add('testing');
137
+
138
+ // Import-based domains
139
+ if (imports.some(i => i.includes('tauri'))) domains.add('desktop-runtime');
140
+ if (imports.some(i => i.includes('tiptap') || i.includes('prosemirror'))) domains.add('text-editing');
141
+ if (imports.some(i => i.includes('react'))) domains.add('frontend-ui');
142
+
143
+ // Name-based domains
144
+ if (/file|save|export|read|write/i.test(name)) domains.add('file-management');
145
+ if (/login|user|session/i.test(name)) domains.add('authentication');
146
+
147
+ return Array.from(domains);
148
+ }
149
+
150
+ /**
151
+ * Infer technical patterns (e.g. hooks, async-io, event-handlers)
152
+ */
153
+ inferTechnicalPatterns(func) {
154
+ const patterns = new Set();
155
+ const name = func.name.toLowerCase();
156
+ const code = func.code || '';
157
+
158
+ if (name.startsWith('use')) patterns.add('react-hooks');
159
+ if (code.includes('async') || code.includes('await')) patterns.add('async-io');
160
+ if (code.includes('on(') || code.includes('addListener') || name.startsWith('handle')) patterns.add('event-driven');
161
+ if (code.includes('new ') || code.includes('class ')) patterns.add('object-oriented');
162
+ if (func.isExported) patterns.add('public-api');
163
+
164
+ return Array.from(patterns);
117
165
  }
118
166
 
119
167
  /**
@@ -179,9 +227,9 @@ export default class HeuristicsManager {
179
227
  conditions = [conditions]
180
228
  }
181
229
 
182
- // For React hook pattern, we need AND logic (all conditions must be true)
183
- // For other patterns, we use OR logic (any condition can be true)
184
- const needsAndLogic = this.requiresAndLogic(conditions)
230
+ // For purpose heuristics, we generally want high precision, so use AND logic
231
+ // if there are multiple conditions
232
+ const needsAndLogic = conditions.length > 1 || this.requiresAndLogic(conditions)
185
233
 
186
234
  if (needsAndLogic) {
187
235
  return conditions.every(condition => {
@@ -277,6 +325,22 @@ export default class HeuristicsManager {
277
325
  return pathParts.includes(includesMatch[1])
278
326
  }
279
327
  }
328
+
329
+ // New: Check imports in the chunk context
330
+ if (condition.includes('chunk.imports.includes(')) {
331
+ const importMatch = condition.match(/chunk\.imports\.includes\(['"]([^'"]+)['"]\)/)
332
+ if (importMatch && func.includes?.imports) {
333
+ return func.includes.imports.some(imp => imp.includes(importMatch[1]))
334
+ }
335
+ }
336
+
337
+ // New: Check for specific naming patterns (case-insensitive)
338
+ if (condition.includes('name.matches(')) {
339
+ const regexMatch = condition.match(/name\.matches\(['"]([^'"]+)['"]\)/)
340
+ if (regexMatch && name) {
341
+ return new RegExp(regexMatch[1], 'i').test(name)
342
+ }
343
+ }
280
344
 
281
345
  return false
282
346
  }
@@ -299,6 +363,36 @@ export default class HeuristicsManager {
299
363
  purpose: "React hook",
300
364
  confidence: 0.9
301
365
  },
366
+ serviceLayer: {
367
+ conditions: ["pathParts.includes('services')"],
368
+ purpose: "Service layer logic",
369
+ confidence: 0.7
370
+ },
371
+ componentLayer: {
372
+ conditions: ["pathParts.includes('components')"],
373
+ purpose: "UI component logic",
374
+ confidence: 0.7
375
+ },
376
+ tauriCommand: {
377
+ conditions: ["name.matches('command')", "chunk.imports.includes('tauri')"],
378
+ purpose: "Tauri backend command",
379
+ confidence: 0.9
380
+ },
381
+ textEditing: {
382
+ conditions: ["chunk.imports.includes('tiptap')", "chunk.imports.includes('prosemirror')"],
383
+ purpose: "Rich text editing logic",
384
+ confidence: 0.95
385
+ },
386
+ fileManagement: {
387
+ conditions: ["pathParts.includes('services')", "name.matches('file|export|save')"],
388
+ purpose: "File system service",
389
+ confidence: 0.85
390
+ },
391
+ uiComponent: {
392
+ conditions: ["pathParts.includes('components')", "name.matches('modal|button|menu|bar')"],
393
+ purpose: "UI component logic",
394
+ confidence: 0.85
395
+ },
302
396
  apiHandler: {
303
397
  conditions: ["name.includes('api')", "name.includes('endpoint')"],
304
398
  purpose: "API handler",
@@ -398,17 +492,25 @@ export default class HeuristicsManager {
398
492
  }
399
493
 
400
494
  /**
401
- * Update configuration (for API endpoints)
495
+ * Update configuration (for API endpoints) and persist to disk
402
496
  */
403
497
  async updateConfig(newConfig) {
404
- this.validateConfig(newConfig)
405
-
406
- // TODO: Implement file writing when API endpoints are created
407
- // For now, just update in-memory config
408
- this.config = newConfig
409
- this.cache.clear()
410
-
411
- return true
498
+ try {
499
+ this.validateConfig(newConfig)
500
+
501
+ // Write to disk
502
+ writeFileSync(this.configPath, JSON.stringify(newConfig, null, 2), 'utf8')
503
+
504
+ this.config = newConfig
505
+ this.cache.clear()
506
+ this.lastLoaded = Date.now()
507
+
508
+ console.log('📝 Heuristics configuration updated and saved to disk')
509
+ return true
510
+ } catch (error) {
511
+ console.error('❌ Failed to update heuristics config:', error.message)
512
+ throw error
513
+ }
412
514
  }
413
515
 
414
516
  /**
package/lib/mcp-server.js CHANGED
@@ -1,5 +1,6 @@
1
- import { readFileSync, writeFileSync, existsSync } from 'fs';
2
- import { join, relative } from 'path';
1
+ import { readFileSync, writeFileSync, existsSync, statSync, mkdirSync, copyFileSync } from 'fs';
2
+ import { join, relative, dirname } from 'path';
3
+ import fs from 'fs';
3
4
  import AgentRuntime from './agent-runtime.js';
4
5
 
5
6
  export class MCPServer {
@@ -54,6 +55,12 @@ export class MCPServer {
54
55
  case 'tools/call':
55
56
  return this.handleCallTool(params, id);
56
57
 
58
+ case 'prompts/list':
59
+ return this.handleListPrompts(id);
60
+
61
+ case 'prompts/get':
62
+ return this.handleGetPrompt(params, id);
63
+
57
64
  case 'prompts/list':
58
65
  return this.createErrorResponse(id, -32601, 'Method not found');
59
66
 
@@ -159,6 +166,62 @@ export class MCPServer {
159
166
  }
160
167
  }
161
168
 
169
+ // List available prompts
170
+ handleListPrompts(id) {
171
+ const prompts = [
172
+ {
173
+ name: 'onboard-me',
174
+ description: 'Guided walkthrough of the current codebase architecture.',
175
+ arguments: []
176
+ },
177
+ {
178
+ name: 'refactor-planner',
179
+ description: 'Plan a refactor by analyzing semantic dependencies.',
180
+ arguments: [
181
+ { name: 'target', description: 'The function or file to refactor', required: true }
182
+ ]
183
+ }
184
+ ];
185
+ return this.createSuccessResponse(id, { prompts });
186
+ }
187
+
188
+ // Get a specific prompt
189
+ handleGetPrompt(params, id) {
190
+ const { name, arguments: args } = params;
191
+
192
+ if (name === 'onboard-me') {
193
+ return this.createSuccessResponse(id, {
194
+ description: 'Codebase Onboarding',
195
+ messages: [
196
+ {
197
+ role: 'user',
198
+ content: {
199
+ type: 'text',
200
+ text: 'Please perform a comprehensive discovery of this codebase using the `agent/discover` tool. Focus on identifying the primary entry points and the core architectural patterns. Use the results to explain how the project is organized.'
201
+ }
202
+ }
203
+ ]
204
+ });
205
+ }
206
+
207
+ if (name === 'refactor-planner') {
208
+ return this.createSuccessResponse(id, {
209
+ description: 'Refactor Planning',
210
+ messages: [
211
+ {
212
+ role: 'user',
213
+ content: {
214
+ type: 'text',
215
+ text: `I want to refactor ${args.target}. Use the \`agent/query\` tool to find all semantic chunks related to this target and the \`agent/investigate\` tool to identify potential impact on other modules. Present a step-by-step refactoring plan.`
216
+ }
217
+ }
218
+ ]
219
+ });
220
+ }
221
+
222
+ return this.createErrorResponse(id, -32602, 'Prompt not found');
223
+ }
224
+
162
225
  // List available tools
163
226
  handleListTools(id) {
164
227
  const tools = [
@@ -912,22 +975,20 @@ ${Array.from(this.cntxServer.bundles.entries()).map(([name, bundle]) =>
912
975
  return this.createErrorResponse(id, -32602, `Bundle '${name}' already exists`);
913
976
  }
914
977
 
915
- // Load current config
916
- const configPath = join(this.cntxServer.CNTX_DIR, 'config.json');
917
- let config = { bundles: {} };
918
-
919
- if (existsSync(configPath)) {
920
- config = JSON.parse(readFileSync(configPath, 'utf8'));
921
- }
922
-
923
- // Add new bundle
924
- config.bundles[name] = patterns;
978
+ // Create bundle in bundle-states.json (single source of truth)
979
+ this.cntxServer.configManager.bundleStates.set(name, {
980
+ patterns: patterns,
981
+ files: [],
982
+ content: '',
983
+ changed: false,
984
+ size: 0,
985
+ generated: null
986
+ });
925
987
 
926
- // Save config
927
- writeFileSync(configPath, JSON.stringify(config, null, 2));
988
+ // Save bundle states
989
+ this.cntxServer.configManager.saveBundleStates();
928
990
 
929
- // Reload server config
930
- this.cntxServer.loadConfig();
991
+ // Regenerate bundles
931
992
  this.cntxServer.generateAllBundles();
932
993
 
933
994
  const result = {
@@ -969,18 +1030,15 @@ ${Array.from(this.cntxServer.bundles.entries()).map(([name, bundle]) =>
969
1030
  return this.createErrorResponse(id, -32602, 'Cannot update master bundle');
970
1031
  }
971
1032
 
972
- // Load current config
973
- const configPath = join(this.cntxServer.CNTX_DIR, 'config.json');
974
- const config = JSON.parse(readFileSync(configPath, 'utf8'));
975
-
976
- // Update bundle patterns
977
- config.bundles[name] = patterns;
1033
+ // Update bundle in bundle-states.json (single source of truth)
1034
+ const bundle = this.cntxServer.configManager.bundleStates.get(name);
1035
+ bundle.patterns = patterns;
1036
+ bundle.changed = true;
978
1037
 
979
- // Save config
980
- writeFileSync(configPath, JSON.stringify(config, null, 2));
1038
+ // Save bundle states
1039
+ this.cntxServer.configManager.saveBundleStates();
981
1040
 
982
- // Reload server config
983
- this.cntxServer.loadConfig();
1041
+ // Regenerate bundles
984
1042
  this.cntxServer.generateAllBundles();
985
1043
 
986
1044
  const result = {
@@ -1022,18 +1080,13 @@ ${Array.from(this.cntxServer.bundles.entries()).map(([name, bundle]) =>
1022
1080
  return this.createErrorResponse(id, -32602, 'Cannot delete master bundle');
1023
1081
  }
1024
1082
 
1025
- // Load current config
1026
- const configPath = join(this.cntxServer.CNTX_DIR, 'config.json');
1027
- const config = JSON.parse(readFileSync(configPath, 'utf8'));
1028
-
1029
- // Remove bundle
1030
- delete config.bundles[name];
1083
+ // Remove bundle from bundle-states.json (single source of truth)
1084
+ this.cntxServer.configManager.bundleStates.delete(name);
1031
1085
 
1032
- // Save config
1033
- writeFileSync(configPath, JSON.stringify(config, null, 2));
1086
+ // Save bundle states
1087
+ this.cntxServer.configManager.saveBundleStates();
1034
1088
 
1035
- // Reload server config
1036
- this.cntxServer.loadConfig();
1089
+ // Regenerate bundles
1037
1090
  this.cntxServer.generateAllBundles();
1038
1091
 
1039
1092
  const result = {
@@ -1188,36 +1241,53 @@ ${Array.from(this.cntxServer.bundles.entries()).map(([name, bundle]) =>
1188
1241
 
1189
1242
  // New tool implementations
1190
1243
  async toolReadFile(args, id) {
1191
- const { path, includeMetadata = true } = args;
1244
+ const { path: filePath, includeMetadata = true } = args;
1192
1245
 
1193
- if (!path) {
1246
+ if (!filePath) {
1194
1247
  return this.createErrorResponse(id, -32602, 'Path is required');
1195
1248
  }
1196
1249
 
1197
1250
  try {
1198
- const fullPath = join(this.cntxServer.CWD, path);
1251
+ const fullPath = join(this.cntxServer.CWD, filePath);
1199
1252
 
1200
1253
  if (!existsSync(fullPath)) {
1201
- return this.createErrorResponse(id, -32602, `File not found: ${path}`);
1254
+ return this.createErrorResponse(id, -32602, `File not found: ${filePath}`);
1202
1255
  }
1203
1256
 
1204
1257
  const content = readFileSync(fullPath, 'utf8');
1205
- const result = { path, content };
1258
+
1259
+ // Fetch semantic context for this file
1260
+ const chunks = this.cntxServer.databaseManager.getChunksByFile(filePath);
1261
+ const totalComplexity = chunks.reduce((sum, c) => sum + (c.complexity?.score || 0), 0);
1262
+ const avgComplexity = chunks.length > 0 ? Math.round(totalComplexity / chunks.length) : 0;
1263
+
1264
+ let semanticHeader = `--- SEMANTIC CONTEXT ---\n`;
1265
+ semanticHeader += `File importance: ${chunks.length} semantic chunks found.\n`;
1266
+ semanticHeader += `Aggregate Complexity: ${totalComplexity} (Avg: ${avgComplexity})\n`;
1267
+ if (chunks.length > 0) {
1268
+ semanticHeader += `Primary purposes: ${[...new Set(chunks.map(c => c.purpose))].join(', ')}\n`;
1269
+ }
1270
+ semanticHeader += `------------------------\n\n`;
1271
+
1272
+ const result = {
1273
+ path: filePath,
1274
+ content: semanticHeader + content
1275
+ };
1206
1276
 
1207
1277
  if (includeMetadata) {
1208
- const stats = require('fs').statSync(fullPath);
1278
+ const stats = fs.statSync(fullPath);
1209
1279
  const bundles = [];
1210
1280
 
1211
1281
  // Find which bundles include this file
1212
1282
  this.cntxServer.bundles.forEach((bundle, name) => {
1213
- if (bundle.files && bundle.files.includes(fullPath)) {
1283
+ if (bundle.files && bundle.files.includes(filePath)) {
1214
1284
  bundles.push(name);
1215
1285
  }
1216
1286
  });
1217
1287
 
1218
1288
  result.metadata = {
1219
1289
  size: stats.size,
1220
- mimeType: this.getMimeType(path),
1290
+ mimeType: this.getMimeType(filePath),
1221
1291
  modified: stats.mtime.toISOString(),
1222
1292
  lines: content.split('\n').length,
1223
1293
  bundles: bundles
@@ -1225,7 +1295,7 @@ ${Array.from(this.cntxServer.bundles.entries()).map(([name, bundle]) =>
1225
1295
  }
1226
1296
 
1227
1297
  return this.createSuccessResponse(id, {
1228
- contents: [{
1298
+ content: [{
1229
1299
  type: 'text',
1230
1300
  text: JSON.stringify(result, null, 2)
1231
1301
  }]
@@ -1236,25 +1306,25 @@ ${Array.from(this.cntxServer.bundles.entries()).map(([name, bundle]) =>
1236
1306
  }
1237
1307
 
1238
1308
  async toolWriteFile(args, id) {
1239
- const { path, content, backup = true, createDirs = true } = args;
1309
+ const { path: filePath, content, backup = true, createDirs = true } = args;
1240
1310
 
1241
- if (!path || content === undefined) {
1311
+ if (!filePath || content === undefined) {
1242
1312
  return this.createErrorResponse(id, -32602, 'Path and content are required');
1243
1313
  }
1244
1314
 
1245
1315
  try {
1246
- const fullPath = join(this.cntxServer.CWD, path);
1247
- const parentDir = require('path').dirname(fullPath);
1316
+ const fullPath = join(this.cntxServer.CWD, filePath);
1317
+ const parentDir = dirname(fullPath);
1248
1318
 
1249
1319
  // Create parent directories if needed
1250
1320
  if (createDirs && !existsSync(parentDir)) {
1251
- require('fs').mkdirSync(parentDir, { recursive: true });
1321
+ fs.mkdirSync(parentDir, { recursive: true });
1252
1322
  }
1253
1323
 
1254
1324
  // Create backup if file exists
1255
1325
  if (backup && existsSync(fullPath)) {
1256
1326
  const backupPath = `${fullPath}.backup.${Date.now()}`;
1257
- require('fs').copyFileSync(fullPath, backupPath);
1327
+ fs.copyFileSync(fullPath, backupPath);
1258
1328
  }
1259
1329
 
1260
1330
  // Write the file
@@ -1269,13 +1339,13 @@ ${Array.from(this.cntxServer.bundles.entries()).map(([name, bundle]) =>
1269
1339
  }
1270
1340
  });
1271
1341
 
1272
- const stats = require('fs').statSync(fullPath);
1342
+ const stats = fs.statSync(fullPath);
1273
1343
 
1274
1344
  return this.createSuccessResponse(id, {
1275
- contents: [{
1345
+ content: [{
1276
1346
  type: 'text',
1277
1347
  text: JSON.stringify({
1278
- path,
1348
+ path: filePath,
1279
1349
  written: true,
1280
1350
  size: stats.size,
1281
1351
  modified: stats.mtime.toISOString()
@@ -1390,7 +1460,7 @@ ${Array.from(this.cntxServer.bundles.entries()).map(([name, bundle]) =>
1390
1460
  }
1391
1461
 
1392
1462
  return this.createSuccessResponse(id, {
1393
- contents: [{
1463
+ content: [{
1394
1464
  type: 'text',
1395
1465
  text: JSON.stringify(result, null, 2)
1396
1466
  }]