cntx-ui 2.0.3 → 2.0.5

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/server.js CHANGED
@@ -4,6 +4,7 @@ import { createServer } from 'http';
4
4
  import { WebSocketServer } from 'ws';
5
5
  import { fileURLToPath } from 'url';
6
6
  import path from 'path';
7
+ import { startMCPTransport } from './lib/mcp-transport.js';
7
8
 
8
9
  const __dirname = dirname(fileURLToPath(import.meta.url));
9
10
 
@@ -32,6 +33,7 @@ export class CntxServer {
32
33
  this.HIDDEN_FILES_CONFIG = join(this.CNTX_DIR, 'hidden-files.json');
33
34
  this.IGNORE_FILE = join(cwd, '.cntxignore');
34
35
  this.CURSOR_RULES_FILE = join(cwd, '.cursorrules');
36
+ this.CLAUDE_MD_FILE = join(cwd, 'CLAUDE.md');
35
37
 
36
38
  this.bundles = new Map();
37
39
  this.ignorePatterns = [];
@@ -215,8 +217,67 @@ export class CntxServer {
215
217
 
216
218
  loadIgnorePatterns() {
217
219
  const systemPatterns = [
220
+ // Version control
218
221
  '**/.git/**',
222
+ '**/.svn/**',
223
+ '**/.hg/**',
224
+
225
+ // Dependencies
219
226
  '**/node_modules/**',
227
+ '**/vendor/**',
228
+ '**/.pnp/**',
229
+
230
+ // Build outputs
231
+ '**/dist/**',
232
+ '**/build/**',
233
+ '**/out/**',
234
+ '**/.next/**',
235
+ '**/.nuxt/**',
236
+ '**/target/**',
237
+
238
+ // Package files
239
+ '**/*.tgz',
240
+ '**/*.tar.gz',
241
+ '**/*.zip',
242
+ '**/*.rar',
243
+ '**/*.7z',
244
+
245
+ // Logs
246
+ '**/*.log',
247
+ '**/logs/**',
248
+
249
+ // Cache directories
250
+ '**/.cache/**',
251
+ '**/.parcel-cache/**',
252
+ '**/.nyc_output/**',
253
+ '**/coverage/**',
254
+ '**/.pytest_cache/**',
255
+ '**/__pycache__/**',
256
+
257
+ // IDE/Editor files
258
+ '**/.vscode/**',
259
+ '**/.idea/**',
260
+ '**/*.swp',
261
+ '**/*.swo',
262
+ '**/*~',
263
+
264
+ // OS files
265
+ '**/.DS_Store',
266
+ '**/Thumbs.db',
267
+ '**/desktop.ini',
268
+
269
+ // Environment files
270
+ '**/.env',
271
+ '**/.env.local',
272
+ '**/.env.*.local',
273
+
274
+ // Lock files
275
+ '**/package-lock.json',
276
+ '**/yarn.lock',
277
+ '**/pnpm-lock.yaml',
278
+ '**/Cargo.lock',
279
+
280
+ // cntx-ui specific
220
281
  '**/.cntx/**'
221
282
  ];
222
283
 
@@ -496,6 +557,92 @@ Add your specific project rules and preferences below:
496
557
  writeFileSync(this.CURSOR_RULES_FILE, content, 'utf8');
497
558
  }
498
559
 
560
+ loadClaudeMd() {
561
+ if (existsSync(this.CLAUDE_MD_FILE)) {
562
+ return readFileSync(this.CLAUDE_MD_FILE, 'utf8');
563
+ }
564
+ return this.getDefaultClaudeMd();
565
+ }
566
+
567
+ getDefaultClaudeMd() {
568
+ // Get project info for context
569
+ let projectInfo = { name: 'unknown', description: '', type: 'general' };
570
+ const pkgPath = join(this.CWD, 'package.json');
571
+
572
+ if (existsSync(pkgPath)) {
573
+ try {
574
+ const pkg = JSON.parse(readFileSync(pkgPath, 'utf8'));
575
+ projectInfo = {
576
+ name: pkg.name || 'unknown',
577
+ description: pkg.description || '',
578
+ type: this.detectProjectType(pkg)
579
+ };
580
+ } catch (e) {
581
+ // Use defaults if package.json is invalid
582
+ }
583
+ }
584
+
585
+ return this.generateClaudeMdTemplate(projectInfo);
586
+ }
587
+
588
+ generateClaudeMdTemplate(projectInfo) {
589
+ const { name, description, type } = projectInfo;
590
+
591
+ let template = `# ${name}
592
+
593
+ ${description ? `${description}\n\n` : ''}## Project Structure
594
+
595
+ This project uses cntx-ui for bundle management and AI context organization.
596
+
597
+ ### Bundles
598
+
599
+ `;
600
+
601
+ // Add bundle information
602
+ this.bundles.forEach((bundle, bundleName) => {
603
+ template += `- **${bundleName}**: ${bundle.files.length} files\n`;
604
+ });
605
+
606
+ template += `
607
+ ### Development Guidelines
608
+
609
+ - Follow the existing code style and patterns
610
+ - Use TypeScript for type safety
611
+ - Write meaningful commit messages
612
+ - Test changes thoroughly
613
+
614
+ ### Key Files
615
+
616
+ - \`.cntx/config.json\` - Bundle configuration
617
+ - \`.cursorrules\` - AI assistant rules
618
+ - \`CLAUDE.md\` - Project context for Claude
619
+ `;
620
+
621
+ if (type === 'react') {
622
+ template += `
623
+ ### React Specific
624
+
625
+ - Use functional components with hooks
626
+ - Follow React best practices
627
+ - Use TypeScript interfaces for props
628
+ `;
629
+ } else if (type === 'node') {
630
+ template += `
631
+ ### Node.js Specific
632
+
633
+ - Use ES modules (import/export)
634
+ - Follow async/await patterns
635
+ - Proper error handling
636
+ `;
637
+ }
638
+
639
+ return template;
640
+ }
641
+
642
+ saveClaudeMd(content) {
643
+ writeFileSync(this.CLAUDE_MD_FILE, content, 'utf8');
644
+ }
645
+
499
646
  shouldIgnoreFile(filePath) {
500
647
  const relativePath = relative(this.CWD, filePath).replace(/\\\\/g, '/');
501
648
 
@@ -730,6 +877,7 @@ Add your specific project rules and preferences below:
730
877
  <cntx:bundle xmlns:cntx="https://cntx.dev/schema" name="${bundleName}" generated="${new Date().toISOString()}">
731
878
  `;
732
879
 
880
+ // Project information
733
881
  const pkgPath = join(this.CWD, 'package.json');
734
882
  if (existsSync(pkgPath)) {
735
883
  try {
@@ -748,27 +896,66 @@ Add your specific project rules and preferences below:
748
896
  }
749
897
  }
750
898
 
751
- xml += ` <cntx:files count="${files.length}">
899
+ // Bundle overview section
900
+ const filesByType = this.categorizeFiles(files);
901
+ const entryPoints = this.identifyEntryPoints(files);
902
+
903
+ xml += ` <cntx:overview>
904
+ <cntx:purpose>${this.escapeXml(this.getBundlePurpose(bundleName))}</cntx:purpose>
905
+ <cntx:file-types>
752
906
  `;
753
907
 
754
- files.forEach(file => {
755
- const fullPath = join(this.CWD, file);
756
- xml += ` <cntx:file path="${file}" ext="${extname(file)}">
908
+ Object.entries(filesByType).forEach(([type, typeFiles]) => {
909
+ xml += ` <cntx:type name="${type}" count="${typeFiles.length}" />
757
910
  `;
911
+ });
758
912
 
759
- try {
760
- const stat = statSync(fullPath);
761
- const content = readFileSync(fullPath, 'utf8');
762
- xml += ` <cntx:meta size="${stat.size}" modified="${stat.mtime.toISOString()}" />
763
- <cntx:content><![CDATA[${content}]]></cntx:content>
913
+ xml += ` </cntx:file-types>
764
914
  `;
765
- } catch (e) {
766
- xml += ` <cntx:error>Could not read file: ${e.message}</cntx:error>
915
+
916
+ if (entryPoints.length > 0) {
917
+ xml += ` <cntx:entry-points>
918
+ `;
919
+ entryPoints.forEach(file => {
920
+ xml += ` <cntx:file>${file}</cntx:file>
921
+ `;
922
+ });
923
+ xml += ` </cntx:entry-points>
924
+ `;
925
+ }
926
+
927
+ xml += ` </cntx:overview>
928
+ `;
929
+
930
+ // Files organized by type
931
+ xml += ` <cntx:files count="${files.length}">
767
932
  `;
768
- }
769
933
 
770
- xml += ` </cntx:file>
934
+ // Entry points first
935
+ if (entryPoints.length > 0) {
936
+ xml += ` <cntx:group type="entry-points" description="Main entry files for this bundle">
937
+ `;
938
+ entryPoints.forEach(file => {
939
+ xml += this.generateFileXML(file);
940
+ });
941
+ xml += ` </cntx:group>
771
942
  `;
943
+ }
944
+
945
+ // Then organize by file type
946
+ Object.entries(filesByType).forEach(([type, typeFiles]) => {
947
+ if (type === 'entry-points') return; // Already handled above
948
+
949
+ const remainingFiles = typeFiles.filter(file => !entryPoints.includes(file));
950
+ if (remainingFiles.length > 0) {
951
+ xml += ` <cntx:group type="${type}" description="${this.getTypeDescription(type)}">
952
+ `;
953
+ remainingFiles.forEach(file => {
954
+ xml += this.generateFileXML(file);
955
+ });
956
+ xml += ` </cntx:group>
957
+ `;
958
+ }
772
959
  });
773
960
 
774
961
  xml += ` </cntx:files>
@@ -776,6 +963,156 @@ Add your specific project rules and preferences below:
776
963
  return xml;
777
964
  }
778
965
 
966
+ categorizeFiles(files) {
967
+ const categories = {
968
+ 'components': [],
969
+ 'hooks': [],
970
+ 'utilities': [],
971
+ 'configuration': [],
972
+ 'styles': [],
973
+ 'types': [],
974
+ 'tests': [],
975
+ 'documentation': [],
976
+ 'other': []
977
+ };
978
+
979
+ files.forEach(file => {
980
+ const ext = extname(file).toLowerCase();
981
+ const basename = file.toLowerCase();
982
+
983
+ if (basename.includes('component') || file.includes('/components/') ||
984
+ ext === '.jsx' || ext === '.tsx' && !basename.includes('test')) {
985
+ categories.components.push(file);
986
+ } else if (basename.includes('hook') || file.includes('/hooks/')) {
987
+ categories.hooks.push(file);
988
+ } else if (basename.includes('util') || file.includes('/utils/') ||
989
+ basename.includes('helper') || file.includes('/lib/')) {
990
+ categories.utilities.push(file);
991
+ } else if (ext === '.json' || basename.includes('config') ||
992
+ ext === '.yaml' || ext === '.yml' || ext === '.toml') {
993
+ categories.configuration.push(file);
994
+ } else if (ext === '.css' || ext === '.scss' || ext === '.less') {
995
+ categories.styles.push(file);
996
+ } else if (basename.includes('type') || ext === '.d.ts' ||
997
+ file.includes('/types/')) {
998
+ categories.types.push(file);
999
+ } else if (basename.includes('test') || basename.includes('spec') ||
1000
+ file.includes('/test/') || file.includes('/__tests__/')) {
1001
+ categories.tests.push(file);
1002
+ } else if (ext === '.md' || basename.includes('readme') ||
1003
+ basename.includes('doc')) {
1004
+ categories.documentation.push(file);
1005
+ } else {
1006
+ categories.other.push(file);
1007
+ }
1008
+ });
1009
+
1010
+ // Remove empty categories
1011
+ Object.keys(categories).forEach(key => {
1012
+ if (categories[key].length === 0) {
1013
+ delete categories[key];
1014
+ }
1015
+ });
1016
+
1017
+ return categories;
1018
+ }
1019
+
1020
+ identifyEntryPoints(files) {
1021
+ const entryPoints = [];
1022
+
1023
+ files.forEach(file => {
1024
+ const basename = file.toLowerCase();
1025
+
1026
+ // Common entry point patterns
1027
+ if (basename.includes('main.') || basename.includes('index.') ||
1028
+ basename.includes('app.') || basename === 'server.js' ||
1029
+ file.endsWith('/App.tsx') || file.endsWith('/App.jsx') ||
1030
+ file.endsWith('/main.tsx') || file.endsWith('/main.js') ||
1031
+ file.endsWith('/index.tsx') || file.endsWith('/index.js')) {
1032
+ entryPoints.push(file);
1033
+ }
1034
+ });
1035
+
1036
+ return entryPoints;
1037
+ }
1038
+
1039
+ getBundlePurpose(bundleName) {
1040
+ const purposes = {
1041
+ 'master': 'Complete project overview with all source files',
1042
+ 'frontend': 'User interface components, pages, and client-side logic',
1043
+ 'backend': 'Server-side logic, APIs, and backend services',
1044
+ 'api': 'API endpoints, routes, and server communication logic',
1045
+ 'server': 'Main server application and core backend functionality',
1046
+ 'components': 'Reusable UI components and interface elements',
1047
+ 'ui-components': 'User interface components and design system elements',
1048
+ 'config': 'Configuration files, settings, and environment setup',
1049
+ 'docs': 'Documentation, README files, and project guides',
1050
+ 'utils': 'Utility functions, helpers, and shared libraries',
1051
+ 'types': 'TypeScript type definitions and interfaces',
1052
+ 'tests': 'Test files, test utilities, and testing configuration'
1053
+ };
1054
+
1055
+ return purposes[bundleName] || `Bundle containing ${bundleName}-related files`;
1056
+ }
1057
+
1058
+ getTypeDescription(type) {
1059
+ const descriptions = {
1060
+ 'components': 'React/UI components and interface elements',
1061
+ 'hooks': 'Custom React hooks and state management',
1062
+ 'utilities': 'Helper functions, utilities, and shared libraries',
1063
+ 'configuration': 'Configuration files, settings, and build configs',
1064
+ 'styles': 'CSS, SCSS, and styling files',
1065
+ 'types': 'TypeScript type definitions and interfaces',
1066
+ 'tests': 'Test files and testing utilities',
1067
+ 'documentation': 'README files, docs, and guides',
1068
+ 'other': 'Additional project files'
1069
+ };
1070
+
1071
+ return descriptions[type] || `Files categorized as ${type}`;
1072
+ }
1073
+
1074
+ generateFileXML(file) {
1075
+ const fullPath = join(this.CWD, file);
1076
+ let fileXml = ` <cntx:file path="${file}" ext="${extname(file)}">
1077
+ `;
1078
+
1079
+ try {
1080
+ const stat = statSync(fullPath);
1081
+ const content = readFileSync(fullPath, 'utf8');
1082
+
1083
+ // Add role indicator for certain files
1084
+ const role = this.getFileRole(file);
1085
+ const roleAttr = role ? ` role="${role}"` : '';
1086
+
1087
+ fileXml = ` <cntx:file path="${file}" ext="${extname(file)}"${roleAttr}>
1088
+ `;
1089
+ fileXml += ` <cntx:meta size="${stat.size}" modified="${stat.mtime.toISOString()}" lines="${content.split('\n').length}" />
1090
+ <cntx:content><![CDATA[${content}]]></cntx:content>
1091
+ `;
1092
+ } catch (e) {
1093
+ fileXml += ` <cntx:error>Could not read file: ${e.message}</cntx:error>
1094
+ `;
1095
+ }
1096
+
1097
+ fileXml += ` </cntx:file>
1098
+ `;
1099
+ return fileXml;
1100
+ }
1101
+
1102
+ getFileRole(file) {
1103
+ const basename = file.toLowerCase();
1104
+
1105
+ if (basename.includes('main.') || basename.includes('index.')) return 'entry-point';
1106
+ if (basename.includes('app.')) return 'main-component';
1107
+ if (file === 'server.js') return 'server-entry';
1108
+ if (basename.includes('config')) return 'configuration';
1109
+ if (basename.includes('package.json')) return 'package-config';
1110
+ if (basename.includes('readme')) return 'documentation';
1111
+
1112
+ return null;
1113
+ }
1114
+
1115
+
779
1116
  escapeXml(text) {
780
1117
  return String(text)
781
1118
  .replace(/&/g, '&amp;')
@@ -1067,6 +1404,27 @@ Add your specific project rules and preferences below:
1067
1404
  };
1068
1405
  res.end(JSON.stringify(templates));
1069
1406
 
1407
+ } else if (url.pathname === '/api/claude-md') {
1408
+ if (req.method === 'GET') {
1409
+ res.writeHead(200, { 'Content-Type': 'text/plain' });
1410
+ const claudeMd = this.loadClaudeMd();
1411
+ res.end(claudeMd);
1412
+ } else if (req.method === 'POST') {
1413
+ let body = '';
1414
+ req.on('data', chunk => body += chunk);
1415
+ req.on('end', () => {
1416
+ try {
1417
+ const { content } = JSON.parse(body);
1418
+ this.saveClaudeMd(content);
1419
+ res.writeHead(200);
1420
+ res.end('OK');
1421
+ } catch (e) {
1422
+ res.writeHead(400);
1423
+ res.end('Invalid request');
1424
+ }
1425
+ });
1426
+ }
1427
+
1070
1428
  } else if (url.pathname === '/api/test-pattern') {
1071
1429
  if (req.method === 'POST') {
1072
1430
  let body = '';
@@ -1222,6 +1580,26 @@ Add your specific project rules and preferences below:
1222
1580
 
1223
1581
  res.end(JSON.stringify(stats));
1224
1582
 
1583
+ } else if (url.pathname.startsWith('/api/bundle-categories/')) {
1584
+ const bundleName = url.pathname.split('/').pop();
1585
+ const bundle = this.bundles.get(bundleName);
1586
+
1587
+ if (bundle) {
1588
+ const filesByType = this.categorizeFiles(bundle.files);
1589
+ const entryPoints = this.identifyEntryPoints(bundle.files);
1590
+
1591
+ res.writeHead(200, { 'Content-Type': 'application/json' });
1592
+ res.end(JSON.stringify({
1593
+ purpose: this.getBundlePurpose(bundleName),
1594
+ filesByType,
1595
+ entryPoints,
1596
+ totalFiles: bundle.files.length
1597
+ }));
1598
+ } else {
1599
+ res.writeHead(404);
1600
+ res.end('Bundle not found');
1601
+ }
1602
+
1225
1603
  } else if (url.pathname === '/api/reset-hidden-files') {
1226
1604
  if (req.method === 'POST') {
1227
1605
  let body = '';
@@ -1305,6 +1683,13 @@ export function startServer(options = {}) {
1305
1683
  return server.startServer(options.port);
1306
1684
  }
1307
1685
 
1686
+ export function startMCPServer(options = {}) {
1687
+ const server = new CntxServer(options.cwd);
1688
+ server.init();
1689
+ startMCPTransport(server);
1690
+ return server;
1691
+ }
1692
+
1308
1693
  export function generateBundle(name = 'master', cwd = process.cwd()) {
1309
1694
  const server = new CntxServer(cwd);
1310
1695
  server.init();