cntx-ui 3.0.7 → 3.0.9
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/dist/bin/cntx-ui.js +70 -0
- package/dist/lib/agent-runtime.js +269 -0
- package/dist/lib/agent-tools.js +162 -0
- package/dist/lib/api-router.js +387 -0
- package/dist/lib/bundle-manager.js +236 -0
- package/dist/lib/configuration-manager.js +230 -0
- package/dist/lib/database-manager.js +277 -0
- package/dist/lib/file-system-manager.js +305 -0
- package/dist/lib/function-level-chunker.js +144 -0
- package/dist/lib/heuristics-manager.js +491 -0
- package/dist/lib/mcp-server.js +159 -0
- package/dist/lib/mcp-transport.js +10 -0
- package/dist/lib/semantic-splitter.js +335 -0
- package/dist/lib/simple-vector-store.js +98 -0
- package/dist/lib/treesitter-semantic-chunker.js +277 -0
- package/dist/lib/websocket-manager.js +268 -0
- package/dist/server.js +225 -0
- package/package.json +18 -8
- package/bin/cntx-ui-mcp.sh +0 -3
- package/bin/cntx-ui.js +0 -123
- package/lib/agent-runtime.js +0 -371
- package/lib/agent-tools.js +0 -370
- package/lib/api-router.js +0 -1026
- package/lib/bundle-manager.js +0 -326
- package/lib/configuration-manager.js +0 -760
- package/lib/database-manager.js +0 -397
- package/lib/file-system-manager.js +0 -489
- package/lib/function-level-chunker.js +0 -406
- package/lib/heuristics-manager.js +0 -529
- package/lib/mcp-server.js +0 -1380
- package/lib/mcp-transport.js +0 -97
- package/lib/semantic-splitter.js +0 -304
- package/lib/simple-vector-store.js +0 -108
- package/lib/treesitter-semantic-chunker.js +0 -1485
- package/lib/websocket-manager.js +0 -470
- package/server.js +0 -687
|
@@ -0,0 +1,387 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* API Router for cntx-ui
|
|
3
|
+
* Handles all HTTP API endpoints and request routing
|
|
4
|
+
*/
|
|
5
|
+
import { readFileSync, writeFileSync, existsSync } from 'fs';
|
|
6
|
+
import path, { join } from 'path';
|
|
7
|
+
export default class APIRouter {
|
|
8
|
+
cntxServer;
|
|
9
|
+
configManager;
|
|
10
|
+
bundleManager;
|
|
11
|
+
fileSystemManager;
|
|
12
|
+
semanticAnalysisManager;
|
|
13
|
+
vectorStore;
|
|
14
|
+
activityManager;
|
|
15
|
+
constructor(cntxServer, configManager, bundleManager, fileSystemManager, semanticAnalysisManager, vectorStore, activityManager) {
|
|
16
|
+
this.cntxServer = cntxServer;
|
|
17
|
+
this.configManager = configManager;
|
|
18
|
+
this.bundleManager = bundleManager;
|
|
19
|
+
this.fileSystemManager = fileSystemManager;
|
|
20
|
+
this.semanticAnalysisManager = semanticAnalysisManager;
|
|
21
|
+
this.vectorStore = vectorStore;
|
|
22
|
+
this.activityManager = activityManager;
|
|
23
|
+
}
|
|
24
|
+
async handleRequest(req, res, url) {
|
|
25
|
+
const { pathname } = url;
|
|
26
|
+
const method = req.method;
|
|
27
|
+
try {
|
|
28
|
+
// === Bundle Endpoints ===
|
|
29
|
+
if (pathname === '/api/bundles' && method === 'GET') {
|
|
30
|
+
return await this.handleGetBundles(req, res, url);
|
|
31
|
+
}
|
|
32
|
+
if (pathname === '/api/bundles' && method === 'POST') {
|
|
33
|
+
return await this.handlePostBundles(req, res);
|
|
34
|
+
}
|
|
35
|
+
if (pathname.startsWith('/api/bundles/') && method === 'GET') {
|
|
36
|
+
const bundleName = pathname.split('/')[3];
|
|
37
|
+
return await this.handleGetBundle(req, res, bundleName);
|
|
38
|
+
}
|
|
39
|
+
if (pathname.startsWith('/api/regenerate/') && (method === 'GET' || method === 'POST')) {
|
|
40
|
+
const bundleName = pathname.split('/')[3];
|
|
41
|
+
return await this.handleRegenerateBundle(req, res, bundleName);
|
|
42
|
+
}
|
|
43
|
+
// === File Endpoints ===
|
|
44
|
+
if (pathname === '/api/files' && method === 'GET') {
|
|
45
|
+
return await this.handleGetFiles(req, res);
|
|
46
|
+
}
|
|
47
|
+
if (pathname.startsWith('/api/files/') && method === 'GET') {
|
|
48
|
+
const filePath = pathname.substring(11); // Remove /api/files/
|
|
49
|
+
return await this.handleGetFile(req, res, filePath);
|
|
50
|
+
}
|
|
51
|
+
if (pathname === '/api/open-file' && method === 'POST') {
|
|
52
|
+
return await this.handlePostOpenFile(req, res);
|
|
53
|
+
}
|
|
54
|
+
// === Configuration Endpoints ===
|
|
55
|
+
if (pathname === '/api/config' && method === 'GET') {
|
|
56
|
+
return await this.handleGetConfig(req, res);
|
|
57
|
+
}
|
|
58
|
+
if (pathname === '/api/config' && method === 'POST') {
|
|
59
|
+
return await this.handlePostConfig(req, res);
|
|
60
|
+
}
|
|
61
|
+
if (pathname === '/api/cntxignore' && method === 'GET') {
|
|
62
|
+
return await this.handleGetCntxignore(req, res);
|
|
63
|
+
}
|
|
64
|
+
if (pathname === '/api/cntxignore' && method === 'POST') {
|
|
65
|
+
return await this.handlePostCntxignore(req, res);
|
|
66
|
+
}
|
|
67
|
+
// === Semantic Analysis Endpoints ===
|
|
68
|
+
if (pathname === '/api/semantic-chunks' && method === 'GET') {
|
|
69
|
+
return await this.handleGetSemanticChunks(req, res, url);
|
|
70
|
+
}
|
|
71
|
+
if (pathname === '/api/semantic-search' && method === 'POST') {
|
|
72
|
+
return await this.handlePostSemanticSearch(req, res);
|
|
73
|
+
}
|
|
74
|
+
// === Vector DB Endpoints ===
|
|
75
|
+
if (pathname === '/api/vector-db/status' && method === 'GET') {
|
|
76
|
+
return await this.handleGetVectorDbStatus(req, res);
|
|
77
|
+
}
|
|
78
|
+
if (pathname === '/api/vector-db/rebuild' && method === 'POST') {
|
|
79
|
+
return await this.handlePostVectorDbRebuild(req, res);
|
|
80
|
+
}
|
|
81
|
+
if (pathname === '/api/vector-db/search' && method === 'POST') {
|
|
82
|
+
return await this.handlePostVectorDbSearch(req, res);
|
|
83
|
+
}
|
|
84
|
+
if (pathname === '/api/vector-db/network' && method === 'GET') {
|
|
85
|
+
return await this.handleGetVectorDbNetwork(req, res);
|
|
86
|
+
}
|
|
87
|
+
// === Database Endpoints ===
|
|
88
|
+
if (pathname === '/api/database/info' && method === 'GET') {
|
|
89
|
+
return await this.handleGetDatabaseInfo(req, res);
|
|
90
|
+
}
|
|
91
|
+
if (pathname === '/api/database/query' && method === 'POST') {
|
|
92
|
+
return await this.handlePostDatabaseQuery(req, res);
|
|
93
|
+
}
|
|
94
|
+
// === Activity Endpoints ===
|
|
95
|
+
if (pathname === '/api/activities' && method === 'GET') {
|
|
96
|
+
return await this.handleGetActivities(req, res);
|
|
97
|
+
}
|
|
98
|
+
if (pathname.startsWith('/api/activities/') && pathname.endsWith('/reasoning') && method === 'GET') {
|
|
99
|
+
const activityId = pathname.split('/')[3];
|
|
100
|
+
return await this.handleGetActivityReasoning(req, res, activityId);
|
|
101
|
+
}
|
|
102
|
+
// === Status & MCP ===
|
|
103
|
+
if (pathname === '/api/status' && method === 'GET') {
|
|
104
|
+
return await this.handleGetStatus(req, res);
|
|
105
|
+
}
|
|
106
|
+
if (pathname === '/api/mcp-status' && method === 'GET') {
|
|
107
|
+
return await this.handleGetMcpStatus(req, res);
|
|
108
|
+
}
|
|
109
|
+
// === Rule Management ===
|
|
110
|
+
if (pathname === '/api/cursor-rules' && method === 'GET') {
|
|
111
|
+
return await this.handleGetCursorRules(req, res);
|
|
112
|
+
}
|
|
113
|
+
if (pathname === '/api/cursor-rules' && method === 'POST') {
|
|
114
|
+
return await this.handlePostCursorRules(req, res);
|
|
115
|
+
}
|
|
116
|
+
if (pathname === '/api/claude-md' && method === 'GET') {
|
|
117
|
+
return await this.handleGetClaudeMd(req, res);
|
|
118
|
+
}
|
|
119
|
+
if (pathname === '/api/claude-md' && method === 'POST') {
|
|
120
|
+
return await this.handlePostClaudeMd(req, res);
|
|
121
|
+
}
|
|
122
|
+
// 404 for unknown API routes
|
|
123
|
+
this.sendError(res, 404, `API endpoint not found: ${method} ${pathname}`);
|
|
124
|
+
}
|
|
125
|
+
catch (error) {
|
|
126
|
+
console.error(`API Error: ${error.message}`);
|
|
127
|
+
this.sendError(res, 500, error.message);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
// === Helper Methods ===
|
|
131
|
+
sendResponse(res, status, data) {
|
|
132
|
+
res.writeHead(status, { 'Content-Type': 'application/json' });
|
|
133
|
+
res.end(JSON.stringify(data));
|
|
134
|
+
}
|
|
135
|
+
sendError(res, status, message) {
|
|
136
|
+
this.sendResponse(res, status, { error: message });
|
|
137
|
+
}
|
|
138
|
+
async getRequestBody(req) {
|
|
139
|
+
return new Promise((resolve, reject) => {
|
|
140
|
+
let body = '';
|
|
141
|
+
req.on('data', chunk => { body += chunk.toString(); });
|
|
142
|
+
req.on('end', () => { resolve(body); });
|
|
143
|
+
req.on('error', reject);
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
// === Handlers ===
|
|
147
|
+
async handleGetBundles(req, res, url) {
|
|
148
|
+
try {
|
|
149
|
+
const bundleInfo = this.bundleManager.getAllBundleInfo();
|
|
150
|
+
this.sendResponse(res, 200, bundleInfo);
|
|
151
|
+
}
|
|
152
|
+
catch (error) {
|
|
153
|
+
this.sendError(res, 500, error.message);
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
async handlePostBundles(req, res) {
|
|
157
|
+
const body = await this.getRequestBody(req);
|
|
158
|
+
const { action, bundleName, fileName, fileNames } = JSON.parse(body);
|
|
159
|
+
if (!action || !bundleName) {
|
|
160
|
+
return this.sendError(res, 400, 'Missing required fields: action and bundleName');
|
|
161
|
+
}
|
|
162
|
+
const bundles = this.configManager.getBundles();
|
|
163
|
+
const bundle = bundles.get(bundleName);
|
|
164
|
+
if (!bundle) {
|
|
165
|
+
return this.sendError(res, 404, `Bundle not found: ${bundleName}`);
|
|
166
|
+
}
|
|
167
|
+
switch (action) {
|
|
168
|
+
case 'add-file':
|
|
169
|
+
if (!fileName)
|
|
170
|
+
return this.sendError(res, 400, 'Missing fileName');
|
|
171
|
+
const relAdd = fileName.startsWith('/') ? path.relative(this.configManager.CWD, fileName) : fileName;
|
|
172
|
+
if (!bundle.files.includes(relAdd)) {
|
|
173
|
+
bundle.files.push(relAdd);
|
|
174
|
+
bundle.changed = true;
|
|
175
|
+
this.configManager.saveBundleStates();
|
|
176
|
+
}
|
|
177
|
+
break;
|
|
178
|
+
case 'remove-file':
|
|
179
|
+
if (!fileName)
|
|
180
|
+
return this.sendError(res, 400, 'Missing fileName');
|
|
181
|
+
const relRem = fileName.startsWith('/') ? path.relative(this.configManager.CWD, fileName) : fileName;
|
|
182
|
+
const idx = bundle.files.indexOf(relRem);
|
|
183
|
+
if (idx > -1) {
|
|
184
|
+
bundle.files.splice(idx, 1);
|
|
185
|
+
bundle.changed = true;
|
|
186
|
+
this.configManager.saveBundleStates();
|
|
187
|
+
}
|
|
188
|
+
break;
|
|
189
|
+
}
|
|
190
|
+
this.sendResponse(res, 200, { success: true });
|
|
191
|
+
}
|
|
192
|
+
async handleGetBundle(req, res, bundleName) {
|
|
193
|
+
const content = await this.bundleManager.getBundleContent(bundleName);
|
|
194
|
+
if (content === null)
|
|
195
|
+
return this.sendError(res, 404, 'Bundle not found');
|
|
196
|
+
res.writeHead(200, { 'Content-Type': 'application/xml' });
|
|
197
|
+
res.end(content);
|
|
198
|
+
}
|
|
199
|
+
async handleRegenerateBundle(req, res, bundleName) {
|
|
200
|
+
await this.bundleManager.regenerateBundle(bundleName);
|
|
201
|
+
this.sendResponse(res, 200, { success: true });
|
|
202
|
+
}
|
|
203
|
+
async handleGetFiles(req, res) {
|
|
204
|
+
const files = this.fileSystemManager.getFileTree();
|
|
205
|
+
this.sendResponse(res, 200, files);
|
|
206
|
+
}
|
|
207
|
+
async handleGetFile(req, res, filePath) {
|
|
208
|
+
const fullPath = this.fileSystemManager.fullPath(filePath);
|
|
209
|
+
if (!existsSync(fullPath))
|
|
210
|
+
return this.sendError(res, 404, 'File not found');
|
|
211
|
+
const content = readFileSync(fullPath, 'utf8');
|
|
212
|
+
res.writeHead(200, { 'Content-Type': 'text/plain' });
|
|
213
|
+
res.end(content);
|
|
214
|
+
}
|
|
215
|
+
async handlePostOpenFile(req, res) {
|
|
216
|
+
const body = await this.getRequestBody(req);
|
|
217
|
+
const { filePath, line } = JSON.parse(body);
|
|
218
|
+
// Simple mock for opening in editor
|
|
219
|
+
console.log(`Editor requested for ${filePath}:${line || 1}`);
|
|
220
|
+
this.sendResponse(res, 200, { success: true });
|
|
221
|
+
}
|
|
222
|
+
async handleGetConfig(req, res) {
|
|
223
|
+
const config = this.configManager.loadConfig();
|
|
224
|
+
this.sendResponse(res, 200, config);
|
|
225
|
+
}
|
|
226
|
+
async handlePostConfig(req, res) {
|
|
227
|
+
const body = await this.getRequestBody(req);
|
|
228
|
+
this.configManager.saveConfig(JSON.parse(body));
|
|
229
|
+
this.sendResponse(res, 200, { success: true });
|
|
230
|
+
}
|
|
231
|
+
async handleGetCntxignore(req, res) {
|
|
232
|
+
if (existsSync(this.configManager.IGNORE_FILE)) {
|
|
233
|
+
const content = readFileSync(this.configManager.IGNORE_FILE, 'utf8');
|
|
234
|
+
res.writeHead(200, { 'Content-Type': 'text/plain' });
|
|
235
|
+
res.end(content);
|
|
236
|
+
}
|
|
237
|
+
else {
|
|
238
|
+
res.end('');
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
async handlePostCntxignore(req, res) {
|
|
242
|
+
const body = await this.getRequestBody(req);
|
|
243
|
+
const { content } = JSON.parse(body);
|
|
244
|
+
const success = this.configManager.saveCntxignore(content);
|
|
245
|
+
if (!success)
|
|
246
|
+
return this.sendError(res, 500, 'Failed to save .cntxignore');
|
|
247
|
+
this.fileSystemManager.setIgnorePatterns(this.configManager.ignorePatterns);
|
|
248
|
+
this.sendResponse(res, 200, { success: true });
|
|
249
|
+
}
|
|
250
|
+
async handleGetSemanticChunks(req, res, url) {
|
|
251
|
+
const refresh = url.query?.refresh === 'true';
|
|
252
|
+
let analysis;
|
|
253
|
+
if (refresh) {
|
|
254
|
+
analysis = await this.cntxServer.refreshSemanticAnalysis();
|
|
255
|
+
}
|
|
256
|
+
else {
|
|
257
|
+
analysis = await this.cntxServer.getSemanticAnalysis();
|
|
258
|
+
}
|
|
259
|
+
const chunks = analysis.chunks.map((chunk) => ({
|
|
260
|
+
id: chunk.id || chunk.name,
|
|
261
|
+
name: chunk.name,
|
|
262
|
+
code: chunk.code,
|
|
263
|
+
semanticType: chunk.subtype || chunk.type,
|
|
264
|
+
businessDomain: chunk.businessDomain || [],
|
|
265
|
+
technicalPatterns: chunk.technicalPatterns || [],
|
|
266
|
+
purpose: chunk.purpose || '',
|
|
267
|
+
filePath: chunk.filePath,
|
|
268
|
+
complexity: chunk.complexity || { score: 0, level: 'low' },
|
|
269
|
+
tags: chunk.tags || [],
|
|
270
|
+
startLine: chunk.startLine
|
|
271
|
+
}));
|
|
272
|
+
this.sendResponse(res, 200, { summary: analysis.summary, chunks });
|
|
273
|
+
}
|
|
274
|
+
async handlePostSemanticSearch(req, res) {
|
|
275
|
+
const body = await this.getRequestBody(req);
|
|
276
|
+
const { query, limit = 20 } = JSON.parse(body);
|
|
277
|
+
const results = await this.vectorStore.search(query, { limit });
|
|
278
|
+
this.sendResponse(res, 200, { results });
|
|
279
|
+
}
|
|
280
|
+
async handleGetVectorDbStatus(req, res) {
|
|
281
|
+
const info = this.configManager.dbManager.getInfo();
|
|
282
|
+
this.sendResponse(res, 200, {
|
|
283
|
+
stats: {
|
|
284
|
+
totalChunks: info.chunkCount,
|
|
285
|
+
embeddingCount: info.embeddingCount,
|
|
286
|
+
modelName: this.vectorStore.modelName
|
|
287
|
+
}
|
|
288
|
+
});
|
|
289
|
+
}
|
|
290
|
+
async handlePostVectorDbRebuild(req, res) {
|
|
291
|
+
const analysis = await this.cntxServer.getSemanticAnalysis();
|
|
292
|
+
for (const chunk of analysis.chunks) {
|
|
293
|
+
await this.vectorStore.upsertChunk(chunk);
|
|
294
|
+
}
|
|
295
|
+
const info = this.configManager.dbManager.getInfo();
|
|
296
|
+
this.sendResponse(res, 200, { success: true, embeddingCount: info.embeddingCount });
|
|
297
|
+
}
|
|
298
|
+
async handlePostVectorDbSearch(req, res) {
|
|
299
|
+
const body = await this.getRequestBody(req);
|
|
300
|
+
const { query, limit = 10 } = JSON.parse(body);
|
|
301
|
+
const results = await this.vectorStore.search(query, { limit });
|
|
302
|
+
this.sendResponse(res, 200, results);
|
|
303
|
+
}
|
|
304
|
+
async handleGetVectorDbNetwork(req, res) {
|
|
305
|
+
const chunks = this.configManager.dbManager.db.prepare('SELECT * FROM semantic_chunks').all();
|
|
306
|
+
const embeddings = this.configManager.dbManager.db.prepare('SELECT * FROM vector_embeddings').all();
|
|
307
|
+
const nodes = chunks.map(c => this.configManager.dbManager.mapChunkRow(c)).slice(0, 100);
|
|
308
|
+
const edges = [];
|
|
309
|
+
// Simple pairwise similarity
|
|
310
|
+
this.sendResponse(res, 200, { nodes, edges });
|
|
311
|
+
}
|
|
312
|
+
async handleGetDatabaseInfo(req, res) {
|
|
313
|
+
const info = this.configManager.dbManager.getInfo();
|
|
314
|
+
this.sendResponse(res, 200, info);
|
|
315
|
+
}
|
|
316
|
+
async handlePostDatabaseQuery(req, res) {
|
|
317
|
+
const body = await this.getRequestBody(req);
|
|
318
|
+
const { query } = JSON.parse(body);
|
|
319
|
+
const results = this.configManager.dbManager.query(query);
|
|
320
|
+
this.sendResponse(res, 200, { results });
|
|
321
|
+
}
|
|
322
|
+
async handleGetActivities(req, res) {
|
|
323
|
+
const activities = await this.activityManager.loadActivities();
|
|
324
|
+
this.sendResponse(res, 200, activities);
|
|
325
|
+
}
|
|
326
|
+
async handleGetActivityReasoning(req, res, activityId) {
|
|
327
|
+
const history = this.configManager.dbManager.getSessionHistory(activityId);
|
|
328
|
+
this.sendResponse(res, 200, { history });
|
|
329
|
+
}
|
|
330
|
+
async handleGetStatus(req, res) {
|
|
331
|
+
const bundles = this.bundleManager.getAllBundleInfo();
|
|
332
|
+
this.sendResponse(res, 200, {
|
|
333
|
+
uptime: process.uptime(),
|
|
334
|
+
memory: process.memoryUsage(),
|
|
335
|
+
bundles,
|
|
336
|
+
scanning: this.bundleManager.isScanning,
|
|
337
|
+
totalFiles: this.fileSystemManager.getAllFiles().length,
|
|
338
|
+
mcp: {
|
|
339
|
+
enabled: this.cntxServer.mcpServerStarted,
|
|
340
|
+
available: true
|
|
341
|
+
}
|
|
342
|
+
});
|
|
343
|
+
}
|
|
344
|
+
async handleGetMcpStatus(req, res) {
|
|
345
|
+
const isRunning = this.cntxServer.mcpServerStarted;
|
|
346
|
+
this.sendResponse(res, 200, {
|
|
347
|
+
enabled: isRunning,
|
|
348
|
+
running: isRunning,
|
|
349
|
+
available: true,
|
|
350
|
+
message: isRunning ? 'MCP server is running' : 'MCP server integration available'
|
|
351
|
+
});
|
|
352
|
+
}
|
|
353
|
+
async handleGetCursorRules(req, res) {
|
|
354
|
+
const filePath = join(this.configManager.CWD, '.cursorrules');
|
|
355
|
+
if (existsSync(filePath)) {
|
|
356
|
+
const content = readFileSync(filePath, 'utf8');
|
|
357
|
+
res.writeHead(200, { 'Content-Type': 'text/plain' });
|
|
358
|
+
res.end(content);
|
|
359
|
+
}
|
|
360
|
+
else {
|
|
361
|
+
res.end('');
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
async handlePostCursorRules(req, res) {
|
|
365
|
+
const body = await this.getRequestBody(req);
|
|
366
|
+
const { content } = JSON.parse(body);
|
|
367
|
+
writeFileSync(join(this.configManager.CWD, '.cursorrules'), content, 'utf8');
|
|
368
|
+
this.sendResponse(res, 200, { success: true });
|
|
369
|
+
}
|
|
370
|
+
async handleGetClaudeMd(req, res) {
|
|
371
|
+
const filePath = join(this.configManager.CWD, 'CLAUDE.md');
|
|
372
|
+
if (existsSync(filePath)) {
|
|
373
|
+
const content = readFileSync(filePath, 'utf8');
|
|
374
|
+
res.writeHead(200, { 'Content-Type': 'text/plain' });
|
|
375
|
+
res.end(content);
|
|
376
|
+
}
|
|
377
|
+
else {
|
|
378
|
+
res.end('');
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
async handlePostClaudeMd(req, res) {
|
|
382
|
+
const body = await this.getRequestBody(req);
|
|
383
|
+
const { content } = JSON.parse(body);
|
|
384
|
+
writeFileSync(join(this.configManager.CWD, 'CLAUDE.md'), content, 'utf8');
|
|
385
|
+
this.sendResponse(res, 200, { success: true });
|
|
386
|
+
}
|
|
387
|
+
}
|
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bundle Manager for cntx-ui
|
|
3
|
+
* Handles Smart Dynamic Bundles and traditional XML generation
|
|
4
|
+
*/
|
|
5
|
+
import { readFileSync } from 'fs';
|
|
6
|
+
export default class BundleManager {
|
|
7
|
+
configManager;
|
|
8
|
+
fileSystemManager;
|
|
9
|
+
webSocketManager;
|
|
10
|
+
db;
|
|
11
|
+
verbose;
|
|
12
|
+
_isScanning;
|
|
13
|
+
constructor(configManager, fileSystemManager, verbose = false) {
|
|
14
|
+
this.configManager = configManager;
|
|
15
|
+
this.fileSystemManager = fileSystemManager;
|
|
16
|
+
this.webSocketManager = null;
|
|
17
|
+
this.db = configManager.dbManager;
|
|
18
|
+
this.verbose = verbose;
|
|
19
|
+
this._isScanning = false;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Get all bundle information, including Smart Dynamic Bundles
|
|
23
|
+
*/
|
|
24
|
+
getAllBundleInfo() {
|
|
25
|
+
const manualBundles = Array.from(this.configManager.getBundles().entries()).map(([name, bundle]) => ({
|
|
26
|
+
name,
|
|
27
|
+
fileCount: bundle.files?.length || 0,
|
|
28
|
+
size: bundle.size || 0,
|
|
29
|
+
generated: bundle.generated,
|
|
30
|
+
changed: !!bundle.changed,
|
|
31
|
+
patterns: bundle.patterns,
|
|
32
|
+
type: 'manual'
|
|
33
|
+
}));
|
|
34
|
+
const smartBundles = this.generateSmartBundleDefinitions();
|
|
35
|
+
// Filter out smart bundles that have no files
|
|
36
|
+
const activeSmartBundles = smartBundles.map(b => {
|
|
37
|
+
const files = this.resolveSmartBundle(b.name);
|
|
38
|
+
return {
|
|
39
|
+
...b,
|
|
40
|
+
fileCount: files.length,
|
|
41
|
+
files
|
|
42
|
+
};
|
|
43
|
+
}).filter(b => b.fileCount > 0);
|
|
44
|
+
return [...manualBundles, ...activeSmartBundles];
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Generate Smart Bundle definitions from indexed semantic data
|
|
48
|
+
*/
|
|
49
|
+
generateSmartBundleDefinitions() {
|
|
50
|
+
const smartBundles = [];
|
|
51
|
+
try {
|
|
52
|
+
// 1. Group by Purpose (Heuristics)
|
|
53
|
+
const purposeRows = this.db.db.prepare('SELECT DISTINCT purpose, COUNT(*) as count FROM semantic_chunks GROUP BY purpose').all();
|
|
54
|
+
purposeRows.forEach(row => {
|
|
55
|
+
if (!row.purpose)
|
|
56
|
+
return;
|
|
57
|
+
const name = `smart:${row.purpose.toLowerCase().replace(/\s+/g, '-')}`;
|
|
58
|
+
smartBundles.push({
|
|
59
|
+
name,
|
|
60
|
+
purpose: row.purpose,
|
|
61
|
+
fileCount: row.count,
|
|
62
|
+
size: 0,
|
|
63
|
+
changed: false,
|
|
64
|
+
type: 'smart',
|
|
65
|
+
description: `Automatically grouped by purpose: ${row.purpose}`
|
|
66
|
+
});
|
|
67
|
+
});
|
|
68
|
+
// 2. Group by Component Types (Subtypes)
|
|
69
|
+
const subtypeRows = this.db.db.prepare('SELECT DISTINCT subtype, COUNT(*) as count FROM semantic_chunks GROUP BY subtype').all();
|
|
70
|
+
subtypeRows.forEach(row => {
|
|
71
|
+
if (!row.subtype)
|
|
72
|
+
return;
|
|
73
|
+
const name = `smart:type-${row.subtype.toLowerCase().replace(/_/g, '-')}`;
|
|
74
|
+
smartBundles.push({
|
|
75
|
+
name,
|
|
76
|
+
purpose: row.subtype,
|
|
77
|
+
fileCount: row.count,
|
|
78
|
+
size: 0,
|
|
79
|
+
changed: false,
|
|
80
|
+
type: 'smart',
|
|
81
|
+
description: `All ${row.subtype} elements across the codebase`
|
|
82
|
+
});
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
catch (e) {
|
|
86
|
+
if (this.verbose)
|
|
87
|
+
console.warn('Smart bundle discovery failed:', e.message);
|
|
88
|
+
}
|
|
89
|
+
return smartBundles;
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Resolve files for a bundle (Manual or Smart)
|
|
93
|
+
*/
|
|
94
|
+
async resolveBundleFiles(bundleName) {
|
|
95
|
+
if (bundleName.startsWith('smart:')) {
|
|
96
|
+
return this.resolveSmartBundle(bundleName);
|
|
97
|
+
}
|
|
98
|
+
const bundle = this.configManager.getBundles().get(bundleName);
|
|
99
|
+
if (!bundle)
|
|
100
|
+
return [];
|
|
101
|
+
const allFiles = this.fileSystemManager.getAllFiles();
|
|
102
|
+
return allFiles.filter(file => bundle.patterns.some(pattern => this.fileSystemManager.matchesPattern(file, pattern))).map(f => this.fileSystemManager.relativePath(f));
|
|
103
|
+
}
|
|
104
|
+
/**
|
|
105
|
+
* Resolve a Smart Bundle query against SQLite
|
|
106
|
+
*/
|
|
107
|
+
resolveSmartBundle(bundleName) {
|
|
108
|
+
const query = bundleName.replace('smart:', '');
|
|
109
|
+
let rows = [];
|
|
110
|
+
if (query.startsWith('type-')) {
|
|
111
|
+
const type = query.replace('type-', '').replace(/-/g, '_');
|
|
112
|
+
rows = this.db.db.prepare('SELECT DISTINCT file_path FROM semantic_chunks WHERE LOWER(subtype) = ?').all(type);
|
|
113
|
+
}
|
|
114
|
+
else {
|
|
115
|
+
const purposeRows = this.db.db.prepare('SELECT DISTINCT purpose FROM semantic_chunks').all();
|
|
116
|
+
const matched = purposeRows.find(r => r.purpose?.toLowerCase().replace(/\s+/g, '-') === query);
|
|
117
|
+
if (matched) {
|
|
118
|
+
rows = this.db.db.prepare('SELECT DISTINCT file_path FROM semantic_chunks WHERE purpose = ?').all(matched.purpose);
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
return rows.map(r => r.file_path);
|
|
122
|
+
}
|
|
123
|
+
// === Bundle Generation ===
|
|
124
|
+
async generateAllBundles() {
|
|
125
|
+
this._isScanning = true;
|
|
126
|
+
try {
|
|
127
|
+
const bundles = this.configManager.getBundles();
|
|
128
|
+
for (const [name] of bundles) {
|
|
129
|
+
await this.regenerateBundle(name);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
finally {
|
|
133
|
+
this._isScanning = false;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
async regenerateBundle(bundleName) {
|
|
137
|
+
if (this.verbose)
|
|
138
|
+
console.log(`🔄 Regenerating bundle: ${bundleName}`);
|
|
139
|
+
const files = await this.resolveBundleFiles(bundleName);
|
|
140
|
+
const content = await this.generateBundleXML(bundleName, files);
|
|
141
|
+
const bundleData = {
|
|
142
|
+
files,
|
|
143
|
+
content,
|
|
144
|
+
size: Buffer.byteLength(content, 'utf8'),
|
|
145
|
+
generated: new Date().toISOString(),
|
|
146
|
+
changed: false,
|
|
147
|
+
patterns: [] // Default patterns
|
|
148
|
+
};
|
|
149
|
+
if (!bundleName.startsWith('smart:')) {
|
|
150
|
+
const existing = this.configManager.getBundles().get(bundleName);
|
|
151
|
+
if (existing) {
|
|
152
|
+
bundleData.patterns = existing.patterns;
|
|
153
|
+
}
|
|
154
|
+
this.configManager.getBundles().set(bundleName, bundleData);
|
|
155
|
+
this.configManager.saveBundleStates();
|
|
156
|
+
}
|
|
157
|
+
return bundleData;
|
|
158
|
+
}
|
|
159
|
+
async generateBundleXML(bundleName, relativeFiles) {
|
|
160
|
+
const projectInfo = this.getProjectInfo();
|
|
161
|
+
let xml = `<?xml version="1.0" encoding="UTF-8"?>\n<codebase>\n`;
|
|
162
|
+
xml += ` <project_info>\n <name>${this.escapeXml(projectInfo.name)}</name>\n <bundle>${this.escapeXml(bundleName)}</bundle>\n </project_info>\n`;
|
|
163
|
+
for (const relPath of relativeFiles) {
|
|
164
|
+
xml += await this.generateFileXML(relPath);
|
|
165
|
+
}
|
|
166
|
+
xml += `</codebase>`;
|
|
167
|
+
return xml;
|
|
168
|
+
}
|
|
169
|
+
async generateFileXML(relativePath) {
|
|
170
|
+
try {
|
|
171
|
+
const fullPath = this.fileSystemManager.fullPath(relativePath);
|
|
172
|
+
const content = readFileSync(fullPath, 'utf8');
|
|
173
|
+
const chunks = this.db.getChunksByFile(relativePath);
|
|
174
|
+
let xml = ` <file path="${this.escapeXml(relativePath)}">\n`;
|
|
175
|
+
if (chunks.length > 0) {
|
|
176
|
+
xml += ` <semantic_context>\n`;
|
|
177
|
+
chunks.forEach(c => {
|
|
178
|
+
xml += ` <chunk name="${this.escapeXml(c.name)}" purpose="${this.escapeXml(c.purpose)}" complexity="${c.complexity?.score || 0}" />\n`;
|
|
179
|
+
});
|
|
180
|
+
xml += ` </semantic_context>\n`;
|
|
181
|
+
}
|
|
182
|
+
xml += ` <content><![CDATA[${content}]]></content>\n </file>\n`;
|
|
183
|
+
return xml;
|
|
184
|
+
}
|
|
185
|
+
catch (e) {
|
|
186
|
+
return ` <file path="${this.escapeXml(relativePath)}" error="${this.escapeXml(e.message)}" />\n`;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
getProjectInfo() {
|
|
190
|
+
try {
|
|
191
|
+
const pkg = JSON.parse(readFileSync(this.fileSystemManager.fullPath('package.json'), 'utf8'));
|
|
192
|
+
return { name: pkg.name || 'Unknown', version: pkg.version || '1.0.0' };
|
|
193
|
+
}
|
|
194
|
+
catch {
|
|
195
|
+
return { name: 'Unknown', version: '1.0.0' };
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
escapeXml(text) {
|
|
199
|
+
return String(text).replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"').replace(/'/g, ''');
|
|
200
|
+
}
|
|
201
|
+
getBundleContent(bundleName) {
|
|
202
|
+
if (bundleName.startsWith('smart:')) {
|
|
203
|
+
// For smart bundles, we generate on the fly if not cached
|
|
204
|
+
return this.regenerateBundle(bundleName).then(data => data.content || '');
|
|
205
|
+
}
|
|
206
|
+
const bundle = this.configManager.getBundles().get(bundleName);
|
|
207
|
+
return bundle ? bundle.content || null : null;
|
|
208
|
+
}
|
|
209
|
+
markBundlesChanged(filename) {
|
|
210
|
+
this.configManager.getBundles().forEach((bundle, name) => {
|
|
211
|
+
if (bundle.patterns?.some(p => this.fileSystemManager.matchesPattern(filename, p))) {
|
|
212
|
+
bundle.changed = true;
|
|
213
|
+
}
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
getBundleInfo(bundleName) {
|
|
217
|
+
if (bundleName.startsWith('smart:')) {
|
|
218
|
+
return this.generateSmartBundleDefinitions().find(b => b.name === bundleName);
|
|
219
|
+
}
|
|
220
|
+
const bundle = this.configManager.getBundles().get(bundleName);
|
|
221
|
+
if (!bundle)
|
|
222
|
+
return undefined;
|
|
223
|
+
return {
|
|
224
|
+
name: bundleName,
|
|
225
|
+
fileCount: bundle.files?.length || 0,
|
|
226
|
+
size: bundle.size || 0,
|
|
227
|
+
generated: bundle.generated,
|
|
228
|
+
changed: !!bundle.changed,
|
|
229
|
+
patterns: bundle.patterns,
|
|
230
|
+
type: 'manual'
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
get isScanning() {
|
|
234
|
+
return this._isScanning;
|
|
235
|
+
}
|
|
236
|
+
}
|