cntx-ui 3.0.8 → 3.1.0

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 (36) hide show
  1. package/dist/bin/cntx-ui.js +89 -0
  2. package/dist/lib/agent-runtime.js +269 -0
  3. package/dist/lib/agent-tools.js +162 -0
  4. package/dist/lib/api-router.js +387 -0
  5. package/dist/lib/bundle-manager.js +236 -0
  6. package/dist/lib/configuration-manager.js +230 -0
  7. package/dist/lib/database-manager.js +277 -0
  8. package/dist/lib/file-system-manager.js +305 -0
  9. package/dist/lib/function-level-chunker.js +144 -0
  10. package/dist/lib/heuristics-manager.js +491 -0
  11. package/dist/lib/mcp-server.js +159 -0
  12. package/dist/lib/mcp-transport.js +10 -0
  13. package/dist/lib/semantic-splitter.js +335 -0
  14. package/dist/lib/simple-vector-store.js +98 -0
  15. package/dist/lib/treesitter-semantic-chunker.js +277 -0
  16. package/dist/lib/websocket-manager.js +268 -0
  17. package/dist/server.js +413 -0
  18. package/package.json +17 -8
  19. package/bin/cntx-ui-mcp.sh +0 -3
  20. package/bin/cntx-ui.js +0 -123
  21. package/lib/agent-runtime.js +0 -371
  22. package/lib/agent-tools.js +0 -370
  23. package/lib/api-router.js +0 -1026
  24. package/lib/bundle-manager.js +0 -326
  25. package/lib/configuration-manager.js +0 -760
  26. package/lib/database-manager.js +0 -397
  27. package/lib/file-system-manager.js +0 -489
  28. package/lib/function-level-chunker.js +0 -406
  29. package/lib/heuristics-manager.js +0 -529
  30. package/lib/mcp-server.js +0 -1380
  31. package/lib/mcp-transport.js +0 -97
  32. package/lib/semantic-splitter.js +0 -347
  33. package/lib/simple-vector-store.js +0 -108
  34. package/lib/treesitter-semantic-chunker.js +0 -1557
  35. package/lib/websocket-manager.js +0 -470
  36. package/server.js +0 -687
@@ -0,0 +1,89 @@
1
+ #!/usr/bin/env node
2
+ import { readFileSync, existsSync } from 'fs';
3
+ import { dirname, join } from 'path';
4
+ import { fileURLToPath } from 'url';
5
+ import { startServer, initConfig, getStatus, setupMCP, generateBundle } from '../server.js';
6
+ const __dirname = dirname(fileURLToPath(import.meta.url));
7
+ let packagePath = join(__dirname, '..', 'package.json');
8
+ if (!existsSync(packagePath)) {
9
+ packagePath = join(__dirname, '..', '..', 'package.json');
10
+ }
11
+ const packageJson = JSON.parse(readFileSync(packagePath, 'utf8'));
12
+ const args = process.argv.slice(2);
13
+ const command = args[0] || 'help';
14
+ const isVerbose = args.includes('--verbose');
15
+ // Graceful shutdown
16
+ process.on('SIGINT', () => {
17
+ console.log('\nšŸ‘‹ Shutting down cntx-ui...');
18
+ process.exit(0);
19
+ });
20
+ async function main() {
21
+ try {
22
+ const actualCommand = command === 'w' ? 'watch' : command;
23
+ switch (actualCommand) {
24
+ case 'watch':
25
+ const port = parseInt(args[1]) || 3333;
26
+ // Enable MCP status tracking by default for the web dashboard
27
+ const withMcp = !args.includes('--no-mcp');
28
+ await startServer({ port, withMcp, verbose: isVerbose });
29
+ break;
30
+ case 'init':
31
+ console.log('šŸš€ Initializing cntx-ui...');
32
+ await initConfig();
33
+ break;
34
+ case 'mcp':
35
+ await startServer({ withMcp: true, skipFileWatcher: true, skipBundleGeneration: true });
36
+ break;
37
+ case 'bundle':
38
+ const bundleName = args[1] || 'master';
39
+ try {
40
+ await generateBundle(bundleName);
41
+ console.log(`āœ… Bundle '${bundleName}' generated successfully`);
42
+ }
43
+ catch (error) {
44
+ console.error(`āŒ Failed to generate bundle '${bundleName}': ${error.message}`);
45
+ process.exit(1);
46
+ }
47
+ break;
48
+ case 'status':
49
+ await getStatus();
50
+ break;
51
+ case 'setup-mcp':
52
+ setupMCP();
53
+ break;
54
+ case 'version':
55
+ case '-v':
56
+ case '--version':
57
+ console.log(`v${packageJson.version}`);
58
+ break;
59
+ case 'help':
60
+ default:
61
+ console.log(`
62
+ cntx-ui v${packageJson.version} - Repository Intelligence engine
63
+
64
+ Usage:
65
+ cntx-ui watch [port] Start the visual dashboard and intelligence engine (default: 3333)
66
+ cntx-ui init Initialize cntx-ui configuration in the current directory
67
+ cntx-ui mcp Start the Model Context Protocol (MCP) server on stdio
68
+ cntx-ui bundle [name] Generate specific bundle (default: master)
69
+ cntx-ui status Show current project status
70
+ cntx-ui setup-mcp Add this project to Claude Desktop MCP config
71
+ cntx-ui version Show current version
72
+ cntx-ui help Show this help information
73
+
74
+ Options:
75
+ --verbose Show detailed logging
76
+ --no-mcp Disable MCP server when running watch
77
+ `);
78
+ break;
79
+ }
80
+ }
81
+ catch (error) {
82
+ console.error(`āŒ Error: ${error.message}`);
83
+ if (isVerbose) {
84
+ console.error(error.stack);
85
+ }
86
+ process.exit(1);
87
+ }
88
+ }
89
+ main().catch(console.error);
@@ -0,0 +1,269 @@
1
+ /**
2
+ * Agent Runtime for Codebase Exploration and Development
3
+ * Now stateful with SQLite-based working memory
4
+ */
5
+ import AgentTools from './agent-tools.js';
6
+ import crypto from 'crypto';
7
+ import fs from 'fs';
8
+ import path from 'path';
9
+ import { fileURLToPath } from 'url';
10
+ export class AgentRuntime {
11
+ cntxServer;
12
+ db;
13
+ tools;
14
+ currentSessionId;
15
+ constructor(cntxServer) {
16
+ this.cntxServer = cntxServer;
17
+ this.db = cntxServer.databaseManager;
18
+ this.tools = new AgentTools(cntxServer);
19
+ this.currentSessionId = null;
20
+ }
21
+ /**
22
+ * Initialize or resume a session
23
+ */
24
+ async startSession(id = null, title = 'New Exploration') {
25
+ this.currentSessionId = id || crypto.randomUUID();
26
+ this.db.createSession(this.currentSessionId, title);
27
+ // Refresh manifest when a new session starts
28
+ await this.generateAgentManifest();
29
+ return this.currentSessionId;
30
+ }
31
+ /**
32
+ * Generates a .cntx/AGENT.md manifest for machine consumption
33
+ */
34
+ async generateAgentManifest() {
35
+ const overview = await this.getCodebaseOverview();
36
+ const summary = await this.getSemanticSummary();
37
+ const bundles = await this.analyzeBundles('all');
38
+ // Auto-generate tool reference from MCP server
39
+ let toolsReference = '';
40
+ if (this.cntxServer.mcpServer) {
41
+ const tools = this.cntxServer.mcpServer.getToolDefinitions();
42
+ toolsReference = tools.map(t => {
43
+ let params = [];
44
+ if (t.inputSchema?.properties) {
45
+ params = Object.entries(t.inputSchema.properties).map(([name, prop]) => {
46
+ const isReq = t.inputSchema.required?.includes(name) ? 'required' : 'optional';
47
+ return `\`${name}\` (${prop.type}, ${isReq}): ${prop.description}`;
48
+ });
49
+ }
50
+ return `### \`${t.name}\`\n${t.description}\n${params.length > 0 ? '**Parameters:**\n- ' + params.join('\n- ') : '*No parameters required*'}\n`;
51
+ }).join('\n');
52
+ }
53
+ const manifest = `# šŸ¤– Agent Handshake: ${overview.projectPath.split('/').pop()}
54
+
55
+ ## Project Overview
56
+ - **Path:** \`${overview.projectPath}\`
57
+ - **Total Files:** ${overview.totalFiles}
58
+ - **Semantic Intelligence:** ${summary.totalChunks} persistent chunks indexed.
59
+
60
+ ## Codebase Organization (Bundles)
61
+ ${bundles.map(b => `- **${b.name}**: ${b.purpose} (${b.fileCount} files)`).join('\n')}
62
+
63
+ ## Intelligence Interface (MCP Tools)
64
+ You have access to a specialized "Repository Intelligence" engine. Use these tools for high-signal exploration:
65
+
66
+ ${toolsReference || '*(MCP Server not yet initialized, tools will appear here)*'}
67
+
68
+ ---
69
+
70
+ ## šŸ›  Complete Tool & API Reference
71
+ Refer to the dynamic reference below for full parameter schemas and HTTP fallback endpoints.
72
+
73
+ ${fs.readFileSync(path.join(path.dirname(fileURLToPath(import.meta.url)), '../templates/TOOLS.md'), 'utf8')}
74
+
75
+ ## Working Memory
76
+ This agent is **stateful**. All interactions in this directory are logged to a persistent SQLite database (\`.cntx/bundles.db\`), allowing for context retention across sessions.
77
+
78
+ ---
79
+ *Generated automatically by cntx-ui. Optimized for LLM consumption.*
80
+ `;
81
+ const manifestPath = path.join(this.cntxServer.CNTX_DIR, 'AGENT.md');
82
+ fs.writeFileSync(manifestPath, manifest, 'utf8');
83
+ if (this.cntxServer.verbose)
84
+ console.log('šŸ“„ Agent manifest updated: .cntx/AGENT.md');
85
+ }
86
+ /**
87
+ * Log an interaction to the agent's memory
88
+ */
89
+ async logInteraction(role, content, metadata = {}) {
90
+ if (!this.currentSessionId)
91
+ await this.startSession();
92
+ this.db.addMessage(this.currentSessionId, role, content, metadata);
93
+ }
94
+ /**
95
+ * Discovery Mode: "Tell me about this codebase"
96
+ * Now logs the discovery process to memory
97
+ */
98
+ async discoverCodebase(options = {}) {
99
+ const { scope = 'all', includeDetails = true } = options;
100
+ try {
101
+ await this.logInteraction('agent', `Starting codebase discovery for scope: ${scope}`);
102
+ const discovery = {
103
+ overview: await this.getCodebaseOverview(),
104
+ bundles: await this.analyzeBundles(scope),
105
+ architecture: await this.analyzeArchitecture(),
106
+ patterns: await this.identifyPatterns(),
107
+ recommendations: []
108
+ };
109
+ if (includeDetails) {
110
+ discovery.semanticSummary = await this.getSemanticSummary();
111
+ discovery.fileTypes = await this.analyzeFileTypes();
112
+ discovery.complexity = await this.analyzeComplexity();
113
+ }
114
+ discovery.recommendations = await this.generateDiscoveryRecommendations();
115
+ await this.logInteraction('agent', `Discovery complete. Found ${discovery.overview.totalFiles} files.`, { discovery });
116
+ return discovery;
117
+ }
118
+ catch (error) {
119
+ await this.logInteraction('agent', `Discovery failed: ${error.message}`);
120
+ throw new Error(`Discovery failed: ${error.message}`);
121
+ }
122
+ }
123
+ /**
124
+ * Query Mode: "Where is the user authentication handled?"
125
+ * Now recalls previous context from SQLite
126
+ */
127
+ async answerQuery(question, options = {}) {
128
+ const { maxResults = 10, includeCode = false } = options;
129
+ try {
130
+ await this.logInteraction('user', question);
131
+ // Perform semantic search via Vector Store
132
+ const combinedResults = await this.cntxServer.vectorStore.search(question, { limit: maxResults });
133
+ // Generate contextual answer
134
+ const answer = await this.generateContextualAnswer(question, { chunks: combinedResults, files: [] }, includeCode);
135
+ const response = {
136
+ question,
137
+ answer: answer.response,
138
+ evidence: answer.evidence,
139
+ confidence: answer.confidence,
140
+ relatedFiles: [...new Set(combinedResults.map(c => c.filePath))].slice(0, 5)
141
+ };
142
+ await this.logInteraction('agent', response.answer, { response });
143
+ return response;
144
+ }
145
+ catch (error) {
146
+ throw new Error(`Query failed: ${error.message}`);
147
+ }
148
+ }
149
+ /**
150
+ * Feature Investigation Mode: Now persists the investigation approach
151
+ */
152
+ async investigateFeature(featureDescription, options = {}) {
153
+ const { includeRecommendations = true } = options;
154
+ try {
155
+ await this.logInteraction('user', `Investigating feature: ${featureDescription}`);
156
+ const investigation = {
157
+ feature: featureDescription,
158
+ existing: await this.findExistingImplementations(featureDescription),
159
+ related: await this.findRelatedCode(featureDescription),
160
+ integration: await this.findIntegrationPoints(featureDescription)
161
+ };
162
+ if (includeRecommendations) {
163
+ investigation.approach = await this.suggestImplementationApproach(investigation);
164
+ }
165
+ await this.logInteraction('agent', `Investigation complete for ${featureDescription}`, { investigation });
166
+ return investigation;
167
+ }
168
+ catch (error) {
169
+ throw new Error(`Feature investigation failed: ${error.message}`);
170
+ }
171
+ }
172
+ // --- Helper Methods ---
173
+ async getCodebaseOverview() {
174
+ const bundles = Array.from(this.cntxServer.bundleManager.getAllBundleInfo());
175
+ const totalFiles = bundles.reduce((sum, b) => sum + b.fileCount, 0);
176
+ const totalSize = bundles.reduce((sum, b) => sum + b.size, 0);
177
+ return {
178
+ projectPath: this.cntxServer.CWD,
179
+ totalBundles: bundles.length,
180
+ totalFiles,
181
+ totalSize,
182
+ bundleNames: bundles.map(b => b.name)
183
+ };
184
+ }
185
+ async analyzeBundles(scope) {
186
+ const bundles = this.cntxServer.bundleManager.getAllBundleInfo();
187
+ const filtered = scope === 'all' ? bundles : bundles.filter(b => b.name === scope);
188
+ return filtered.map(b => ({
189
+ ...b,
190
+ purpose: this.inferBundlePurpose(b.name, b.files || [])
191
+ }));
192
+ }
193
+ inferBundlePurpose(name, files) {
194
+ if (name.includes('component') || name.includes('ui'))
195
+ return 'UI Components';
196
+ if (name.includes('api') || name.includes('server'))
197
+ return 'Backend API';
198
+ return 'General Module';
199
+ }
200
+ async analyzeArchitecture() {
201
+ return {
202
+ type: 'Dynamic Architecture',
203
+ timestamp: new Date().toISOString()
204
+ };
205
+ }
206
+ async identifyPatterns() {
207
+ return {
208
+ coding: 'Modern Node.js',
209
+ style: 'Functional / Modular'
210
+ };
211
+ }
212
+ async getSemanticSummary() {
213
+ const chunks = this.db.db.prepare('SELECT COUNT(*) as count FROM semantic_chunks').get();
214
+ return { totalChunks: chunks.count };
215
+ }
216
+ async analyzeFileTypes() {
217
+ const rows = this.db.db.prepare('SELECT file_path FROM semantic_chunks').all();
218
+ const exts = {};
219
+ rows.forEach(r => {
220
+ const ext = r.file_path.split('.').pop() || 'unknown';
221
+ exts[ext] = (exts[ext] || 0) + 1;
222
+ });
223
+ return exts;
224
+ }
225
+ async analyzeComplexity() {
226
+ const rows = this.db.db.prepare('SELECT complexity_score FROM semantic_chunks').all();
227
+ const scores = { low: 0, medium: 0, high: 0 };
228
+ rows.forEach(r => {
229
+ if (r.complexity_score < 5)
230
+ scores.low++;
231
+ else if (r.complexity_score < 15)
232
+ scores.medium++;
233
+ else
234
+ scores.high++;
235
+ });
236
+ return scores;
237
+ }
238
+ async generateDiscoveryRecommendations() {
239
+ return [{ type: 'info', message: 'Continue organizing by semantic purpose.' }];
240
+ }
241
+ async findExistingImplementations(featureDescription) {
242
+ return await this.cntxServer.vectorStore.search(featureDescription, { limit: 5 });
243
+ }
244
+ async findRelatedCode(featureDescription) {
245
+ return [];
246
+ }
247
+ async findIntegrationPoints(featureDescription) {
248
+ return [];
249
+ }
250
+ async suggestImplementationApproach(investigation) {
251
+ return { strategy: 'TBD', description: 'Ready to plan' };
252
+ }
253
+ async generateContextualAnswer(question, results, includeCode) {
254
+ let response = `Based on the codebase analysis:\n\n`;
255
+ if (results.chunks.length > 0) {
256
+ const top = results.chunks[0];
257
+ response += `The most relevant implementation found is \`${top.name}\` in \`${top.filePath}\` (Purpose: ${top.purpose}).\n\n`;
258
+ }
259
+ else {
260
+ response += `No direct semantic matches found. Try refining your query.`;
261
+ }
262
+ return {
263
+ response,
264
+ evidence: results.chunks.slice(0, 3),
265
+ confidence: results.chunks.length > 0 ? 0.8 : 0.2
266
+ };
267
+ }
268
+ }
269
+ export default AgentRuntime;
@@ -0,0 +1,162 @@
1
+ /**
2
+ * Agent Tools for Codebase Exploration
3
+ * Built on top of existing cntx-ui infrastructure
4
+ */
5
+ import { existsSync, statSync } from 'fs';
6
+ import { join, relative } from 'path';
7
+ import path from 'path';
8
+ import { exec } from 'child_process';
9
+ import { promisify } from 'util';
10
+ const execAsync = promisify(exec);
11
+ export default class AgentTools {
12
+ cntxServer;
13
+ constructor(cntxServer) {
14
+ this.cntxServer = cntxServer;
15
+ }
16
+ /**
17
+ * Execute a shell command within the project context
18
+ */
19
+ async executeCommand(command) {
20
+ try {
21
+ const { stdout, stderr } = await execAsync(command, { cwd: this.cntxServer.CWD });
22
+ return { stdout, stderr };
23
+ }
24
+ catch (error) {
25
+ return { error: error.message, stdout: error.stdout, stderr: error.stderr };
26
+ }
27
+ }
28
+ /**
29
+ * List files in a directory, respecting ignore patterns
30
+ */
31
+ async listFiles(dirPath = '.') {
32
+ try {
33
+ const absolutePath = join(this.cntxServer.CWD, dirPath);
34
+ if (!existsSync(absolutePath)) {
35
+ return { error: `Directory not found: ${dirPath}` };
36
+ }
37
+ const files = this.cntxServer.fileSystemManager.getAllFiles(absolutePath);
38
+ return files.map(f => relative(this.cntxServer.CWD, f));
39
+ }
40
+ catch (error) {
41
+ return { error: error.message };
42
+ }
43
+ }
44
+ /**
45
+ * Search for code chunks based on semantic similarity or text matching
46
+ */
47
+ async searchChunks(query, options = {}) {
48
+ const { maxResults = 10 } = options;
49
+ try {
50
+ let chunks = [];
51
+ // Try semantic search if vector store is initialized
52
+ if (this.cntxServer.vectorStoreInitialized) {
53
+ try {
54
+ const searchResults = await this.cntxServer.vectorStore.search(query, { limit: maxResults * 2 });
55
+ const chunkIds = searchResults.map(r => r.id || r.chunkId).filter(Boolean);
56
+ const allChunks = await this.cntxServer.getSemanticAnalysis();
57
+ chunks = allChunks.chunks.filter((c) => chunkIds.includes(c.id));
58
+ }
59
+ catch (error) {
60
+ console.warn('Semantic search failed, falling back to text search:', error);
61
+ }
62
+ }
63
+ // Fallback or combine with text search if needed
64
+ if (chunks.length < maxResults) {
65
+ const allChunks = await this.cntxServer.getSemanticAnalysis();
66
+ const textResults = allChunks.chunks.filter((c) => c.name.toLowerCase().includes(query.toLowerCase()) ||
67
+ c.purpose.toLowerCase().includes(query.toLowerCase()) ||
68
+ c.code.toLowerCase().includes(query.toLowerCase()));
69
+ // Merge and deduplicate
70
+ const seenIds = new Set(chunks.map(c => c.id));
71
+ for (const chunk of textResults) {
72
+ if (!seenIds.has(chunk.id)) {
73
+ chunks.push(chunk);
74
+ if (chunks.length >= maxResults * 2)
75
+ break;
76
+ }
77
+ }
78
+ }
79
+ return chunks.slice(0, maxResults);
80
+ }
81
+ catch (error) {
82
+ return { error: error.message };
83
+ }
84
+ }
85
+ /**
86
+ * Get technical metadata for a file
87
+ */
88
+ async getFileMetadata(filePath) {
89
+ try {
90
+ const fullPath = join(this.cntxServer.CWD, filePath);
91
+ if (!existsSync(fullPath)) {
92
+ return { error: `File not found: ${filePath}` };
93
+ }
94
+ const stats = statSync(fullPath);
95
+ const chunks = this.cntxServer.databaseManager.getChunksByFile(filePath);
96
+ return {
97
+ path: filePath,
98
+ size: stats.size,
99
+ modified: stats.mtime,
100
+ type: this.getFileType(filePath),
101
+ semanticChunks: chunks.length,
102
+ complexity: chunks.reduce((sum, c) => sum + (c.complexity?.score || 0), 0)
103
+ };
104
+ }
105
+ catch (error) {
106
+ return { error: error.message };
107
+ }
108
+ }
109
+ // --- Helper Methods ---
110
+ getFileSize(filePath) {
111
+ try {
112
+ const fullPath = join(this.cntxServer.CWD, filePath);
113
+ const stats = statSync(fullPath);
114
+ return stats.size;
115
+ }
116
+ catch {
117
+ return 0;
118
+ }
119
+ }
120
+ getFileType(filePath) {
121
+ const ext = path.extname(filePath).toLowerCase();
122
+ const typeMap = {
123
+ '.js': 'javascript',
124
+ '.jsx': 'javascript',
125
+ '.ts': 'typescript',
126
+ '.tsx': 'typescript',
127
+ '.py': 'python',
128
+ '.rs': 'rust',
129
+ '.go': 'go',
130
+ '.java': 'java',
131
+ '.cpp': 'cpp',
132
+ '.c': 'c',
133
+ '.md': 'markdown',
134
+ '.json': 'json',
135
+ '.yaml': 'yaml',
136
+ '.yml': 'yaml',
137
+ '.toml': 'toml'
138
+ };
139
+ return typeMap[ext] || 'text';
140
+ }
141
+ getMimeType(filePath) {
142
+ const ext = path.extname(filePath).toLowerCase();
143
+ const mimeTypes = {
144
+ '.js': 'application/javascript',
145
+ '.jsx': 'application/javascript',
146
+ '.ts': 'application/typescript',
147
+ '.tsx': 'application/typescript',
148
+ '.json': 'application/json',
149
+ '.md': 'text/markdown',
150
+ '.html': 'text/html',
151
+ '.css': 'text/css',
152
+ '.txt': 'text/plain',
153
+ '.rs': 'text/x-rust'
154
+ };
155
+ return mimeTypes[ext] || 'text/plain';
156
+ }
157
+ truncateContent(content, maxLength = 1000) {
158
+ if (!content || content.length <= maxLength)
159
+ return content;
160
+ return content.substring(0, maxLength) + '...';
161
+ }
162
+ }