cntx-ui
Version:
Autonomous Repository Intelligence engine with web UI and MCP server. Unified semantic code understanding, local RAG, and agent working memory.
501 lines (498 loc) • 19 kB
JavaScript
/**
* Refactored cntx-ui Server
* Lean orchestration layer using modular architecture
*/
import { createServer } from 'http';
import { join, dirname, extname, basename } from 'path';
import { fileURLToPath, parse } from 'url';
import { existsSync, mkdirSync, readFileSync, writeFileSync, copyFileSync, cpSync } from 'fs';
import { homedir } from 'os';
// Import our modular components
import ConfigurationManager from './lib/configuration-manager.js';
import FileSystemManager from './lib/file-system-manager.js';
import BundleManager from './lib/bundle-manager.js';
import APIRouter from './lib/api-router.js';
import WebSocketManager from './lib/websocket-manager.js';
import ArtifactManager from './lib/artifact-manager.js';
// Import existing lib modules
import SemanticSplitter from './lib/semantic-splitter.js';
import SimpleVectorStore from './lib/simple-vector-store.js';
import { MCPServer } from './lib/mcp-server.js';
import AgentRuntime from './lib/agent-runtime.js';
const __dirname = dirname(fileURLToPath(import.meta.url));
function getProjectName(cwd) {
const packageJsonPath = join(cwd, 'package.json');
if (existsSync(packageJsonPath)) {
try {
const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf8'));
if (typeof packageJson?.name === 'string' && packageJson.name.trim()) {
return packageJson.name.trim();
}
}
catch {
// Fall through to directory name
}
}
return basename(cwd);
}
export class CntxServer {
CWD;
CNTX_DIR;
version;
verbose;
isMcp;
mcpServerStarted;
mcpServer;
initMessages;
configManager;
databaseManager;
fileSystemManager;
bundleManager;
webSocketManager;
apiRouter;
artifactManager;
semanticSplitter;
vectorStore;
agentRuntime;
semanticCache;
lastSemanticAnalysis;
vectorStoreInitialized;
semanticAnalysisManager;
activityManager;
constructor(cwd = process.cwd(), options = {}) {
this.CWD = cwd;
this.CNTX_DIR = join(cwd, '.cntx');
this.verbose = options.verbose || false;
this.isMcp = options.isMcp || false;
this.mcpServerStarted = false;
this.mcpServer = null;
this.initMessages = [];
// Read package version
try {
let pkgDir = __dirname;
let pkgPath = join(pkgDir, 'package.json');
if (!existsSync(pkgPath)) {
pkgDir = join(__dirname, '..');
pkgPath = join(pkgDir, 'package.json');
}
const pkg = JSON.parse(readFileSync(pkgPath, 'utf8'));
this.version = pkg.version;
}
catch {
this.version = '3.1.5';
}
// Ensure directory exists
if (!existsSync(this.CNTX_DIR))
mkdirSync(this.CNTX_DIR, { recursive: true });
// Initialize modular components
this.configManager = new ConfigurationManager(cwd, { verbose: this.verbose });
this.databaseManager = this.configManager.dbManager;
this.fileSystemManager = new FileSystemManager(cwd, { verbose: this.verbose });
this.bundleManager = new BundleManager(this.configManager, this.fileSystemManager, this.verbose);
this.webSocketManager = new WebSocketManager(this.bundleManager, this.configManager, {
verbose: this.verbose,
isMcp: this.isMcp
});
this.artifactManager = new ArtifactManager(cwd);
// AI Components
this.semanticSplitter = new SemanticSplitter({
maxChunkSize: 2000,
includeContext: true,
minFunctionSize: 50,
verbose: this.verbose,
isMcp: this.isMcp
});
this.vectorStore = new SimpleVectorStore(this.databaseManager, {
modelName: 'Xenova/all-MiniLM-L6-v2',
isMcp: this.isMcp
});
this.semanticCache = null;
this.lastSemanticAnalysis = null;
this.vectorStoreInitialized = false;
// Initialize Agent Runtime
this.agentRuntime = new AgentRuntime(this);
this.semanticAnalysisManager = this; // Self as manager
this.activityManager = this; // Simple mock for now
// Initialize API router
this.apiRouter = new APIRouter(this, this.configManager, this.bundleManager, this.fileSystemManager, this.semanticAnalysisManager, this.vectorStore, this.activityManager);
// Cross-module linkage
this.bundleManager.fileSystemManager = this.fileSystemManager;
this.bundleManager.webSocketManager = this.webSocketManager;
}
log(message) {
if (this.isMcp) {
process.stderr.write(message + '\n');
}
else {
console.log(message);
}
}
async init(options = {}) {
const { skipFileWatcher = false, skipBundleGeneration = false } = options;
// Load configs
this.configManager.loadConfig();
this.configManager.loadHiddenFilesConfig();
this.configManager.loadIgnorePatterns();
this.configManager.loadBundleStates();
if (!skipFileWatcher) {
this.startWatching();
// Trigger semantic analysis
if (!this.semanticCache) {
this.getSemanticAnalysis().catch(err => console.error('Initial semantic analysis failed:', err.message));
}
if (!skipBundleGeneration) {
await this.bundleManager.generateAllBundles();
}
}
// Load reasoning/manifest
await this.agentRuntime.generateAgentManifest();
this.artifactManager.refresh();
}
startWatching() {
this.fileSystemManager.startWatching((eventType, filename) => {
this.bundleManager.markBundlesChanged(filename);
this.webSocketManager.onFileChanged(filename, eventType);
});
}
async getSemanticAnalysis() {
// Try SQLite first
try {
const dbChunks = this.databaseManager.db.prepare('SELECT * FROM semantic_chunks').all();
if (dbChunks.length > 0) {
if (!this.semanticCache) {
this.semanticCache = {
chunks: dbChunks.map(row => this.databaseManager.mapChunkRow(row)),
summary: { totalChunks: dbChunks.length }
};
}
return this.semanticCache;
}
}
catch (e) { }
// Fresh analysis
const files = this.fileSystemManager.getAllFiles().map(f => this.fileSystemManager.relativePath(f))
.filter(f => ['.js', '.jsx', '.ts', '.tsx', '.rs', '.json', '.css', '.scss', '.html', '.sql', '.md', '.toml'].includes(extname(f).toLowerCase()));
let bundleConfig = null;
if (existsSync(this.configManager.CONFIG_FILE)) {
bundleConfig = JSON.parse(readFileSync(this.configManager.CONFIG_FILE, 'utf8'));
}
this.semanticCache = await this.semanticSplitter.extractSemanticChunks(this.CWD, files, bundleConfig);
this.lastSemanticAnalysis = Date.now();
if (this.semanticCache.chunks.length > 0) {
this.databaseManager.saveChunks(this.semanticCache.chunks);
}
this.enhanceSemanticChunksIfNeeded(this.semanticCache);
return this.semanticCache;
}
async enhanceSemanticChunksIfNeeded(analysis) {
if (!analysis || !analysis.chunks)
return;
try {
if (!this.vectorStoreInitialized) {
await this.vectorStore.init();
this.vectorStoreInitialized = true;
}
let embedded = 0;
let skipped = 0;
for (const chunk of analysis.chunks) {
if (!this.databaseManager.getEmbedding(chunk.id)) {
try {
await this.vectorStore.upsertChunk(chunk);
embedded++;
}
catch (e) {
skipped++;
}
}
}
if (skipped > 0) {
console.warn(`⚠️ Embedded ${embedded} chunks, skipped ${skipped} (too large or invalid)`);
}
}
catch (e) {
console.error(`⚠️ Vector embedding failed: ${e.message}. Semantic search will be unavailable.`);
}
}
async refreshSemanticAnalysis() {
this.databaseManager.db.prepare('DELETE FROM semantic_chunks').run();
this.databaseManager.db.prepare('DELETE FROM vector_embeddings').run();
this.semanticCache = null;
return this.getSemanticAnalysis();
}
startMCPServer() {
if (!this.mcpServer) {
this.mcpServer = new MCPServer(this, this.version);
this.mcpServerStarted = true;
}
}
async listen(port = 3333, host = 'localhost') {
if (this.isMcp) {
this.log('Mode: MCP (stdio) - Skipping HTTP server (no port fallback required)');
return null;
}
const server = createServer((req, res) => {
const url = parse(req.url || '/', true);
// Serve static files from web/dist
if (!url.pathname?.startsWith('/api/')) {
return this.handleStaticFile(req, res, url);
}
// Route API requests
this.apiRouter.handleRequest(req, res, url);
});
return new Promise((resolve, reject) => {
let currentPort = port;
const maxRetries = 10;
let retries = 0;
const tryListen = (p) => {
server.listen(p, host, () => {
this.webSocketManager.initialize(server);
this.log(`🚀 cntx-ui server running at http://${host}:${p}`);
resolve(server);
});
};
server.on('error', (e) => {
if (e.code === 'EADDRINUSE' && retries < maxRetries) {
retries++;
const failedPort = currentPort;
currentPort++;
this.log(`⚠️ Port ${failedPort} busy, trying ${currentPort}...`);
tryListen(currentPort);
}
else {
reject(e);
}
});
tryListen(currentPort);
});
}
handleStaticFile(req, res, url) {
let webDir = join(__dirname, 'web', 'dist');
if (!existsSync(webDir)) {
// Fallback for dist/ context — web/dist is at package root
webDir = join(__dirname, '..', 'web', 'dist');
}
let filePath = join(webDir, url.pathname === '/' ? 'index.html' : url.pathname);
if (!existsSync(filePath)) {
filePath = join(webDir, 'index.html');
}
try {
const content = readFileSync(filePath);
const ext = extname(filePath).toLowerCase();
const contentType = this.getContentType(ext);
res.writeHead(200, { 'Content-Type': contentType });
res.end(content);
}
catch (e) {
res.writeHead(404);
res.end('Not Found');
}
}
getContentType(ext) {
const types = {
'.html': 'text/html',
'.js': 'application/javascript',
'.css': 'text/css',
'.json': 'application/json',
'.png': 'image/png',
'.jpg': 'image/jpeg',
'.svg': 'image/svg+xml'
};
return types[ext] || 'text/plain';
}
// Activity Manager Mock
async loadActivities() {
return [];
}
}
export async function startServer(options = {}) {
const server = new CntxServer(options.cwd, options);
await server.init(options);
if (options.withMcp !== false)
server.startMCPServer();
return await server.listen(options.port, options.host);
}
// Initialize project configuration
export async function initConfig(cwd = process.cwd()) {
const server = new CntxServer(cwd);
// 1. Initialize directory structure
if (!existsSync(server.CNTX_DIR)) {
mkdirSync(server.CNTX_DIR, { recursive: true });
server.log('📁 Created .cntx directory');
}
// 2. Create .mcp.json for Claude Code discovery
const mcpConfigPath = join(cwd, '.mcp.json');
const mcpConfig = {
mcpServers: {
"cntx-ui": {
command: "cntx-ui",
args: ["mcp"],
cwd: "."
}
}
};
writeFileSync(mcpConfigPath, JSON.stringify(mcpConfig, null, 2), 'utf8');
server.log('📄 Created .mcp.json for agent auto-discovery');
// 3. Initialize basic configuration with better defaults and auto-suggestions
server.configManager.loadConfig();
const suggestedBundles = {
master: ['**/*']
};
// Directory-based auto-suggestions
const commonDirs = [
{ dir: 'src/components', name: 'ui-components' },
{ dir: 'src/services', name: 'services' },
{ dir: 'src/lib', name: 'libraries' },
{ dir: 'src/hooks', name: 'react-hooks' },
{ dir: 'server', name: 'backend-api' },
{ dir: 'tests', name: 'test-suite' }
];
commonDirs.forEach(d => {
if (existsSync(join(cwd, d.dir))) {
suggestedBundles[d.name] = [`${d.dir}/**`];
server.log(`💡 Suggested bundle: ${d.name} (${d.dir}/**)`);
}
});
server.configManager.saveConfig({
bundles: suggestedBundles
});
// 4. Create robust default .cntxignore
const ignorePath = join(cwd, '.cntxignore');
if (!existsSync(ignorePath)) {
const defaultIgnore = `# Binary files
*.db
*.db-journal
*.png
*.jpg
*.jpeg
*.ico
*.icns
*.gif
*.zip
*.tar.gz
# Generated files
**/gen/**
**/dist/**
**/build/**
**/node_modules/**
**/.next/**
**/.cache/**
# cntx-ui internals
.cntx/**
.mcp.json
`;
writeFileSync(ignorePath, defaultIgnore, 'utf8');
server.log('📄 Created .cntxignore with smart defaults');
}
server.log('⚙️ Basic configuration initialized');
let templateDir = join(__dirname, 'templates');
if (!existsSync(templateDir)) {
// Fallback for dist/ context
templateDir = join(__dirname, '..', 'templates');
}
const projectName = getProjectName(cwd);
// Copy agent configuration files
const agentFiles = [
'agent-config.yaml',
'agent-instructions.md'
];
for (const file of agentFiles) {
const sourcePath = join(templateDir, file);
const destPath = join(server.CNTX_DIR, file);
if (existsSync(sourcePath) && !existsSync(destPath)) {
if (file === 'agent-config.yaml') {
const template = readFileSync(sourcePath, 'utf8');
const updated = template.replace(/^project:\s*["'].*?["']\s*$/m, `project: "${projectName}"`);
writeFileSync(destPath, updated, 'utf8');
}
else {
copyFileSync(sourcePath, destPath);
}
server.log(`📄 Created ${file}`);
}
}
// Copy agent-rules directory structure
const agentRulesSource = join(templateDir, 'agent-rules');
const agentRulesDest = join(server.CNTX_DIR, 'agent-rules');
if (existsSync(agentRulesSource) && !existsSync(agentRulesDest)) {
cpSync(agentRulesSource, agentRulesDest, { recursive: true });
server.log('📁 Created agent-rules directory with templates');
}
// 5. Trigger initial semantic scan (master bundle)
server.log('🔪 Performing initial semantic scan...');
await server.init({ skipFileWatcher: true, skipBundleGeneration: true });
await server.bundleManager.regenerateBundle('master');
server.log('✅ Project is ready for AI agents');
return server.initMessages;
}
export async function generateBundle(name) {
const server = new CntxServer(process.cwd());
await server.init({ skipFileWatcher: true });
return await server.bundleManager.regenerateBundle(name);
}
export async function getStatus() {
const server = new CntxServer(process.cwd());
await server.init({ skipFileWatcher: true });
const bundles = server.bundleManager.getAllBundleInfo();
const totalFiles = server.fileSystemManager.getAllFiles().length;
// Resolve actual file counts from glob patterns (getAllBundleInfo only
// returns stored counts which are 0 until bundles are generated)
const bundlesWithCounts = await Promise.all(bundles.map(async (bundle) => {
if (bundle.fileCount === 0 && bundle.patterns?.length) {
const files = await server.bundleManager.resolveBundleFiles(bundle.name);
return { ...bundle, fileCount: files.length };
}
return bundle;
}));
server.log('📊 cntx-ui Status');
server.log('================');
server.log(`Total files: ${totalFiles}`);
server.log(`Bundles: ${bundlesWithCounts.length}`);
bundlesWithCounts.forEach(bundle => {
const sizeStr = bundle.size > 0 ? ` (${Math.round(bundle.size / 1024)}KB)` : '';
server.log(` • ${bundle.name}: ${bundle.fileCount} files${sizeStr}`);
});
return {
totalFiles,
bundles: bundlesWithCounts.length,
bundleDetails: bundlesWithCounts
};
}
export function setupMCP() {
const configPath = join(homedir(), 'Library', 'Application Support', 'Claude', 'claude_desktop_config.json');
const projectPath = process.cwd();
const server = new CntxServer(projectPath);
server.log('🔧 Setting up MCP integration...');
server.log(`Project: ${projectPath}`);
server.log(`Claude config: ${configPath}`);
try {
let config = {};
if (existsSync(configPath)) {
config = JSON.parse(readFileSync(configPath, 'utf8'));
}
if (!config.mcpServers) {
config.mcpServers = {};
}
config.mcpServers['cntx-ui'] = {
command: 'npx',
args: ['cntx-ui', 'mcp'],
cwd: projectPath
};
// Ensure directory exists
mkdirSync(dirname(configPath), { recursive: true });
writeFileSync(configPath, JSON.stringify(config, null, 2));
server.log('✅ MCP integration configured');
server.log('💡 Restart Claude Desktop to apply changes');
}
catch (error) {
console.error('❌ Failed to setup MCP:', error.message);
server.log('💡 You may need to manually add the configuration to Claude Desktop');
}
}
// Auto-start server when run directly
const isMainModule = import.meta.url === `file://${process.argv[1]}`;
if (isMainModule) {
const server = new CntxServer();
server.log('🚀 Starting cntx-ui server...');
server.init();
server.listen(3333, 'localhost');
}