cursor-recursive-rag 0.2.0-alpha.2 → 0.2.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.
- package/README.md +179 -203
- package/dist/adapters/llm/anthropic.d.ts +27 -0
- package/dist/adapters/llm/anthropic.d.ts.map +1 -0
- package/dist/adapters/llm/anthropic.js +287 -0
- package/dist/adapters/llm/anthropic.js.map +1 -0
- package/dist/adapters/llm/base.d.ts +62 -0
- package/dist/adapters/llm/base.d.ts.map +1 -0
- package/dist/adapters/llm/base.js +140 -0
- package/dist/adapters/llm/base.js.map +1 -0
- package/dist/adapters/llm/deepseek.d.ts +24 -0
- package/dist/adapters/llm/deepseek.d.ts.map +1 -0
- package/dist/adapters/llm/deepseek.js +228 -0
- package/dist/adapters/llm/deepseek.js.map +1 -0
- package/dist/adapters/llm/groq.d.ts +25 -0
- package/dist/adapters/llm/groq.d.ts.map +1 -0
- package/dist/adapters/llm/groq.js +265 -0
- package/dist/adapters/llm/groq.js.map +1 -0
- package/dist/adapters/llm/index.d.ts +62 -0
- package/dist/adapters/llm/index.d.ts.map +1 -0
- package/dist/adapters/llm/index.js +380 -0
- package/dist/adapters/llm/index.js.map +1 -0
- package/dist/adapters/llm/ollama.d.ts +23 -0
- package/dist/adapters/llm/ollama.d.ts.map +1 -0
- package/dist/adapters/llm/ollama.js +261 -0
- package/dist/adapters/llm/ollama.js.map +1 -0
- package/dist/adapters/llm/openai.d.ts +22 -0
- package/dist/adapters/llm/openai.d.ts.map +1 -0
- package/dist/adapters/llm/openai.js +232 -0
- package/dist/adapters/llm/openai.js.map +1 -0
- package/dist/adapters/llm/openrouter.d.ts +27 -0
- package/dist/adapters/llm/openrouter.d.ts.map +1 -0
- package/dist/adapters/llm/openrouter.js +305 -0
- package/dist/adapters/llm/openrouter.js.map +1 -0
- package/dist/adapters/vector/index.d.ts.map +1 -1
- package/dist/adapters/vector/index.js +8 -0
- package/dist/adapters/vector/index.js.map +1 -1
- package/dist/adapters/vector/redis-native.d.ts +35 -0
- package/dist/adapters/vector/redis-native.d.ts.map +1 -0
- package/dist/adapters/vector/redis-native.js +170 -0
- package/dist/adapters/vector/redis-native.js.map +1 -0
- package/dist/cli/commands/chat.d.ts +4 -0
- package/dist/cli/commands/chat.d.ts.map +1 -0
- package/dist/cli/commands/chat.js +374 -0
- package/dist/cli/commands/chat.js.map +1 -0
- package/dist/cli/commands/maintenance.d.ts +4 -0
- package/dist/cli/commands/maintenance.d.ts.map +1 -0
- package/dist/cli/commands/maintenance.js +237 -0
- package/dist/cli/commands/maintenance.js.map +1 -0
- package/dist/cli/commands/rules.d.ts +9 -0
- package/dist/cli/commands/rules.d.ts.map +1 -0
- package/dist/cli/commands/rules.js +639 -0
- package/dist/cli/commands/rules.js.map +1 -0
- package/dist/cli/commands/setup.js +5 -4
- package/dist/cli/commands/setup.js.map +1 -1
- package/dist/cli/index.js +6 -0
- package/dist/cli/index.js.map +1 -1
- package/dist/config/memoryConfig.d.ts +427 -0
- package/dist/config/memoryConfig.d.ts.map +1 -0
- package/dist/config/memoryConfig.js +258 -0
- package/dist/config/memoryConfig.js.map +1 -0
- package/dist/config/rulesConfig.d.ts +486 -0
- package/dist/config/rulesConfig.d.ts.map +1 -0
- package/dist/config/rulesConfig.js +345 -0
- package/dist/config/rulesConfig.js.map +1 -0
- package/dist/dashboard/coreTools.d.ts +14 -0
- package/dist/dashboard/coreTools.d.ts.map +1 -0
- package/dist/dashboard/coreTools.js +413 -0
- package/dist/dashboard/coreTools.js.map +1 -0
- package/dist/dashboard/public/index.html +1982 -13
- package/dist/dashboard/server.d.ts +1 -8
- package/dist/dashboard/server.d.ts.map +1 -1
- package/dist/dashboard/server.js +846 -13
- package/dist/dashboard/server.js.map +1 -1
- package/dist/dashboard/toolRegistry.d.ts +192 -0
- package/dist/dashboard/toolRegistry.d.ts.map +1 -0
- package/dist/dashboard/toolRegistry.js +322 -0
- package/dist/dashboard/toolRegistry.js.map +1 -0
- package/dist/proxy/index.d.ts +1 -1
- package/dist/proxy/index.d.ts.map +1 -1
- package/dist/proxy/index.js +9 -6
- package/dist/proxy/index.js.map +1 -1
- package/dist/server/index.js +21 -0
- package/dist/server/index.js.map +1 -1
- package/dist/server/tools/crawl.d.ts.map +1 -1
- package/dist/server/tools/crawl.js +8 -0
- package/dist/server/tools/crawl.js.map +1 -1
- package/dist/server/tools/index.d.ts.map +1 -1
- package/dist/server/tools/index.js +19 -1
- package/dist/server/tools/index.js.map +1 -1
- package/dist/server/tools/ingest.d.ts.map +1 -1
- package/dist/server/tools/ingest.js +5 -0
- package/dist/server/tools/ingest.js.map +1 -1
- package/dist/server/tools/memory.d.ts +250 -0
- package/dist/server/tools/memory.d.ts.map +1 -0
- package/dist/server/tools/memory.js +472 -0
- package/dist/server/tools/memory.js.map +1 -0
- package/dist/server/tools/recursive-query.d.ts.map +1 -1
- package/dist/server/tools/recursive-query.js +6 -0
- package/dist/server/tools/recursive-query.js.map +1 -1
- package/dist/server/tools/search.d.ts.map +1 -1
- package/dist/server/tools/search.js +6 -0
- package/dist/server/tools/search.js.map +1 -1
- package/dist/services/activity-log.d.ts +10 -0
- package/dist/services/activity-log.d.ts.map +1 -0
- package/dist/services/activity-log.js +53 -0
- package/dist/services/activity-log.js.map +1 -0
- package/dist/services/categoryManager.d.ts +110 -0
- package/dist/services/categoryManager.d.ts.map +1 -0
- package/dist/services/categoryManager.js +549 -0
- package/dist/services/categoryManager.js.map +1 -0
- package/dist/services/contextEnvironment.d.ts +206 -0
- package/dist/services/contextEnvironment.d.ts.map +1 -0
- package/dist/services/contextEnvironment.js +481 -0
- package/dist/services/contextEnvironment.js.map +1 -0
- package/dist/services/conversationProcessor.d.ts +99 -0
- package/dist/services/conversationProcessor.d.ts.map +1 -0
- package/dist/services/conversationProcessor.js +311 -0
- package/dist/services/conversationProcessor.js.map +1 -0
- package/dist/services/cursorChatReader.d.ts +129 -0
- package/dist/services/cursorChatReader.d.ts.map +1 -0
- package/dist/services/cursorChatReader.js +419 -0
- package/dist/services/cursorChatReader.js.map +1 -0
- package/dist/services/decayCalculator.d.ts +85 -0
- package/dist/services/decayCalculator.d.ts.map +1 -0
- package/dist/services/decayCalculator.js +182 -0
- package/dist/services/decayCalculator.js.map +1 -0
- package/dist/services/enhancedVectorStore.d.ts +102 -0
- package/dist/services/enhancedVectorStore.d.ts.map +1 -0
- package/dist/services/enhancedVectorStore.js +245 -0
- package/dist/services/enhancedVectorStore.js.map +1 -0
- package/dist/services/hybridScorer.d.ts +120 -0
- package/dist/services/hybridScorer.d.ts.map +1 -0
- package/dist/services/hybridScorer.js +334 -0
- package/dist/services/hybridScorer.js.map +1 -0
- package/dist/services/knowledgeExtractor.d.ts +45 -0
- package/dist/services/knowledgeExtractor.d.ts.map +1 -0
- package/dist/services/knowledgeExtractor.js +436 -0
- package/dist/services/knowledgeExtractor.js.map +1 -0
- package/dist/services/knowledgeStorage.d.ts +102 -0
- package/dist/services/knowledgeStorage.d.ts.map +1 -0
- package/dist/services/knowledgeStorage.js +383 -0
- package/dist/services/knowledgeStorage.js.map +1 -0
- package/dist/services/maintenanceScheduler.d.ts +89 -0
- package/dist/services/maintenanceScheduler.d.ts.map +1 -0
- package/dist/services/maintenanceScheduler.js +479 -0
- package/dist/services/maintenanceScheduler.js.map +1 -0
- package/dist/services/memoryMetadataStore.d.ts +62 -0
- package/dist/services/memoryMetadataStore.d.ts.map +1 -0
- package/dist/services/memoryMetadataStore.js +570 -0
- package/dist/services/memoryMetadataStore.js.map +1 -0
- package/dist/services/recursiveRetrieval.d.ts +122 -0
- package/dist/services/recursiveRetrieval.d.ts.map +1 -0
- package/dist/services/recursiveRetrieval.js +443 -0
- package/dist/services/recursiveRetrieval.js.map +1 -0
- package/dist/services/relationshipGraph.d.ts +77 -0
- package/dist/services/relationshipGraph.d.ts.map +1 -0
- package/dist/services/relationshipGraph.js +411 -0
- package/dist/services/relationshipGraph.js.map +1 -0
- package/dist/services/rlmSafeguards.d.ts +273 -0
- package/dist/services/rlmSafeguards.d.ts.map +1 -0
- package/dist/services/rlmSafeguards.js +705 -0
- package/dist/services/rlmSafeguards.js.map +1 -0
- package/dist/services/rulesAnalyzer.d.ts +119 -0
- package/dist/services/rulesAnalyzer.d.ts.map +1 -0
- package/dist/services/rulesAnalyzer.js +768 -0
- package/dist/services/rulesAnalyzer.js.map +1 -0
- package/dist/services/rulesMerger.d.ts +75 -0
- package/dist/services/rulesMerger.d.ts.map +1 -0
- package/dist/services/rulesMerger.js +404 -0
- package/dist/services/rulesMerger.js.map +1 -0
- package/dist/services/rulesParser.d.ts +127 -0
- package/dist/services/rulesParser.d.ts.map +1 -0
- package/dist/services/rulesParser.js +594 -0
- package/dist/services/rulesParser.js.map +1 -0
- package/dist/services/smartChunker.d.ts +110 -0
- package/dist/services/smartChunker.d.ts.map +1 -0
- package/dist/services/smartChunker.js +520 -0
- package/dist/services/smartChunker.js.map +1 -0
- package/dist/types/categories.d.ts +105 -0
- package/dist/types/categories.d.ts.map +1 -0
- package/dist/types/categories.js +108 -0
- package/dist/types/categories.js.map +1 -0
- package/dist/types/extractedKnowledge.d.ts +233 -0
- package/dist/types/extractedKnowledge.d.ts.map +1 -0
- package/dist/types/extractedKnowledge.js +56 -0
- package/dist/types/extractedKnowledge.js.map +1 -0
- package/dist/types/index.d.ts +9 -2
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/index.js +12 -1
- package/dist/types/index.js.map +1 -1
- package/dist/types/llmProvider.d.ts +282 -0
- package/dist/types/llmProvider.d.ts.map +1 -0
- package/dist/types/llmProvider.js +48 -0
- package/dist/types/llmProvider.js.map +1 -0
- package/dist/types/memory.d.ts +227 -0
- package/dist/types/memory.d.ts.map +1 -0
- package/dist/types/memory.js +76 -0
- package/dist/types/memory.js.map +1 -0
- package/dist/types/relationships.d.ts +167 -0
- package/dist/types/relationships.d.ts.map +1 -0
- package/dist/types/relationships.js +106 -0
- package/dist/types/relationships.js.map +1 -0
- package/dist/types/rulesOptimizer.d.ts +345 -0
- package/dist/types/rulesOptimizer.d.ts.map +1 -0
- package/dist/types/rulesOptimizer.js +22 -0
- package/dist/types/rulesOptimizer.js.map +1 -0
- package/docs/cursor-recursive-rag-memory-spec.md +4569 -0
- package/docs/cursor-recursive-rag-tasks.md +1355 -0
- package/package.json +6 -3
- package/restart-rag.sh +16 -0
package/dist/dashboard/server.js
CHANGED
|
@@ -7,21 +7,19 @@ import { loadConfig, writeConfig } from '../services/config.js';
|
|
|
7
7
|
import { createVectorStore } from '../adapters/vector/index.js';
|
|
8
8
|
import { createEmbedder } from '../adapters/embeddings/index.js';
|
|
9
9
|
import { createOpenSkillsClient } from '../integrations/openskills.js';
|
|
10
|
+
import { logActivity as sharedLogActivity, getActivityLog } from '../services/activity-log.js';
|
|
11
|
+
import { getToolRegistry, JobStatus } from './toolRegistry.js';
|
|
12
|
+
import { registerCoreTools } from './coreTools.js';
|
|
13
|
+
import { loadRulesConfig, saveRulesConfig, validatePattern, testPattern, EXAMPLE_PATTERNS, RulesAnalyzerConfigSchema, LLM_PROVIDERS, } from '../config/rulesConfig.js';
|
|
14
|
+
import { createProvider } from '../adapters/llm/index.js';
|
|
15
|
+
import { RulesParser } from '../services/rulesParser.js';
|
|
16
|
+
import { getRulesAnalyzer } from '../services/rulesAnalyzer.js';
|
|
17
|
+
import { getRulesMerger } from '../services/rulesMerger.js';
|
|
18
|
+
import { writeFileSync, unlinkSync, mkdirSync, copyFileSync } from 'fs';
|
|
10
19
|
const __filename = fileURLToPath(import.meta.url);
|
|
11
20
|
const __dirname = dirname(__filename);
|
|
12
|
-
// In-memory activity log (would use a proper store in production)
|
|
13
|
-
const activityLog = [];
|
|
14
21
|
export function logActivity(type, message, details) {
|
|
15
|
-
|
|
16
|
-
timestamp: new Date().toISOString(),
|
|
17
|
-
type,
|
|
18
|
-
message,
|
|
19
|
-
details
|
|
20
|
-
});
|
|
21
|
-
// Keep only last 100 entries
|
|
22
|
-
if (activityLog.length > 100) {
|
|
23
|
-
activityLog.pop();
|
|
24
|
-
}
|
|
22
|
+
sharedLogActivity(type, message, details);
|
|
25
23
|
}
|
|
26
24
|
const MIME_TYPES = {
|
|
27
25
|
'.html': 'text/html',
|
|
@@ -122,7 +120,41 @@ async function handleAPI(req, res, path) {
|
|
|
122
120
|
return;
|
|
123
121
|
}
|
|
124
122
|
if (path === '/api/activity' && req.method === 'GET') {
|
|
125
|
-
|
|
123
|
+
const activities = getActivityLog();
|
|
124
|
+
res.end(JSON.stringify(activities));
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
if (path === '/api/health' && req.method === 'GET') {
|
|
128
|
+
const config = loadConfig();
|
|
129
|
+
const status = {
|
|
130
|
+
vectorStore: { type: config.vectorStore, status: 'unknown', error: null, count: 0 },
|
|
131
|
+
embeddings: { type: config.embeddings, status: 'unknown', error: null }
|
|
132
|
+
};
|
|
133
|
+
// Test vector store connection
|
|
134
|
+
try {
|
|
135
|
+
const vectorStore = createVectorStore(config.vectorStore, config);
|
|
136
|
+
const count = await vectorStore.count();
|
|
137
|
+
status.vectorStore.status = 'connected';
|
|
138
|
+
status.vectorStore.count = count;
|
|
139
|
+
if (vectorStore.disconnect) {
|
|
140
|
+
await vectorStore.disconnect();
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
catch (e) {
|
|
144
|
+
status.vectorStore.status = 'error';
|
|
145
|
+
status.vectorStore.error = e instanceof Error ? e.message : 'Connection failed';
|
|
146
|
+
}
|
|
147
|
+
// Test embeddings
|
|
148
|
+
try {
|
|
149
|
+
const embedder = await createEmbedder(config.embeddings, config);
|
|
150
|
+
await embedder.embed('test');
|
|
151
|
+
status.embeddings.status = 'connected';
|
|
152
|
+
}
|
|
153
|
+
catch (e) {
|
|
154
|
+
status.embeddings.status = 'error';
|
|
155
|
+
status.embeddings.error = e instanceof Error ? e.message : 'Embeddings failed';
|
|
156
|
+
}
|
|
157
|
+
res.end(JSON.stringify(status));
|
|
126
158
|
return;
|
|
127
159
|
}
|
|
128
160
|
if (path === '/api/search' && req.method === 'POST') {
|
|
@@ -219,6 +251,805 @@ async function handleAPI(req, res, path) {
|
|
|
219
251
|
}
|
|
220
252
|
return;
|
|
221
253
|
}
|
|
254
|
+
// Tools API endpoints
|
|
255
|
+
if (path === '/api/tools' && req.method === 'GET') {
|
|
256
|
+
const registry = getToolRegistry();
|
|
257
|
+
const tools = registry.getTools();
|
|
258
|
+
const categoriesWithCounts = registry.getCategoriesWithCounts();
|
|
259
|
+
res.end(JSON.stringify({
|
|
260
|
+
tools: tools.map(t => ({
|
|
261
|
+
...t,
|
|
262
|
+
schema: registry.getParameterSchema(t.name),
|
|
263
|
+
})),
|
|
264
|
+
categories: Object.entries(categoriesWithCounts).map(([name, count]) => ({
|
|
265
|
+
name,
|
|
266
|
+
count,
|
|
267
|
+
})),
|
|
268
|
+
totalTools: tools.length,
|
|
269
|
+
}));
|
|
270
|
+
return;
|
|
271
|
+
}
|
|
272
|
+
// Get tools by category
|
|
273
|
+
const toolsByCategoryMatch = path.match(/^\/api\/tools\/category\/([^/]+)$/);
|
|
274
|
+
if (toolsByCategoryMatch && req.method === 'GET') {
|
|
275
|
+
const category = toolsByCategoryMatch[1];
|
|
276
|
+
const registry = getToolRegistry();
|
|
277
|
+
const tools = registry.getToolsByCategory(category);
|
|
278
|
+
res.end(JSON.stringify({
|
|
279
|
+
category,
|
|
280
|
+
tools: tools.map(t => ({
|
|
281
|
+
...t,
|
|
282
|
+
schema: registry.getParameterSchema(t.name),
|
|
283
|
+
})),
|
|
284
|
+
}));
|
|
285
|
+
return;
|
|
286
|
+
}
|
|
287
|
+
// Get single tool with schema
|
|
288
|
+
const toolDetailMatch = path.match(/^\/api\/tools\/([^/]+)$/);
|
|
289
|
+
if (toolDetailMatch && req.method === 'GET') {
|
|
290
|
+
const toolName = toolDetailMatch[1];
|
|
291
|
+
const registry = getToolRegistry();
|
|
292
|
+
const tool = registry.getTool(toolName);
|
|
293
|
+
if (!tool) {
|
|
294
|
+
res.statusCode = 404;
|
|
295
|
+
res.end(JSON.stringify({ error: `Tool '${toolName}' not found` }));
|
|
296
|
+
return;
|
|
297
|
+
}
|
|
298
|
+
res.end(JSON.stringify({
|
|
299
|
+
...tool,
|
|
300
|
+
schema: registry.getParameterSchema(toolName),
|
|
301
|
+
}));
|
|
302
|
+
return;
|
|
303
|
+
}
|
|
304
|
+
// Execute tool
|
|
305
|
+
const toolExecuteMatch = path.match(/^\/api\/tools\/([^/]+)\/execute$/);
|
|
306
|
+
if (toolExecuteMatch && req.method === 'POST') {
|
|
307
|
+
const toolName = toolExecuteMatch[1];
|
|
308
|
+
const registry = getToolRegistry();
|
|
309
|
+
if (!registry.hasTool(toolName)) {
|
|
310
|
+
res.statusCode = 404;
|
|
311
|
+
res.end(JSON.stringify({ error: `Tool '${toolName}' not found` }));
|
|
312
|
+
return;
|
|
313
|
+
}
|
|
314
|
+
let body = '';
|
|
315
|
+
req.on('data', chunk => body += chunk);
|
|
316
|
+
req.on('end', async () => {
|
|
317
|
+
try {
|
|
318
|
+
const params = body ? JSON.parse(body) : {};
|
|
319
|
+
const tool = registry.getTool(toolName);
|
|
320
|
+
// For long-running tools, execute async and return job ID
|
|
321
|
+
if (tool?.isLongRunning) {
|
|
322
|
+
const jobId = registry.executeAsync(toolName, params);
|
|
323
|
+
res.end(JSON.stringify({
|
|
324
|
+
async: true,
|
|
325
|
+
jobId,
|
|
326
|
+
message: `Tool '${toolName}' started. Check status at /api/tools/${toolName}/status/${jobId}`,
|
|
327
|
+
}));
|
|
328
|
+
return;
|
|
329
|
+
}
|
|
330
|
+
// Execute synchronously
|
|
331
|
+
const result = await registry.execute(toolName, params);
|
|
332
|
+
logActivity('query', `Tool executed: ${toolName}`, { params, success: result.success });
|
|
333
|
+
res.end(JSON.stringify(result));
|
|
334
|
+
}
|
|
335
|
+
catch (e) {
|
|
336
|
+
res.statusCode = 400;
|
|
337
|
+
res.end(JSON.stringify({ error: e instanceof Error ? e.message : 'Invalid request' }));
|
|
338
|
+
}
|
|
339
|
+
});
|
|
340
|
+
return;
|
|
341
|
+
}
|
|
342
|
+
// Get job status
|
|
343
|
+
const jobStatusMatch = path.match(/^\/api\/tools\/([^/]+)\/status\/([^/]+)$/);
|
|
344
|
+
if (jobStatusMatch && req.method === 'GET') {
|
|
345
|
+
const [, toolName, jobId] = jobStatusMatch;
|
|
346
|
+
const registry = getToolRegistry();
|
|
347
|
+
const job = registry.getJob(jobId);
|
|
348
|
+
if (!job) {
|
|
349
|
+
res.statusCode = 404;
|
|
350
|
+
res.end(JSON.stringify({ error: `Job '${jobId}' not found` }));
|
|
351
|
+
return;
|
|
352
|
+
}
|
|
353
|
+
res.end(JSON.stringify({
|
|
354
|
+
id: job.id,
|
|
355
|
+
toolName: job.toolName,
|
|
356
|
+
status: job.status,
|
|
357
|
+
progress: job.progress,
|
|
358
|
+
progressMessage: job.progressMessage,
|
|
359
|
+
startedAt: job.startedAt.toISOString(),
|
|
360
|
+
completedAt: job.completedAt?.toISOString(),
|
|
361
|
+
result: job.status === JobStatus.COMPLETED || job.status === JobStatus.FAILED ? job.result : undefined,
|
|
362
|
+
}));
|
|
363
|
+
return;
|
|
364
|
+
}
|
|
365
|
+
// Get recent jobs
|
|
366
|
+
if (path === '/api/tools/jobs' && req.method === 'GET') {
|
|
367
|
+
const registry = getToolRegistry();
|
|
368
|
+
const jobs = registry.getRecentJobs(20);
|
|
369
|
+
res.end(JSON.stringify({
|
|
370
|
+
jobs: jobs.map(j => ({
|
|
371
|
+
id: j.id,
|
|
372
|
+
toolName: j.toolName,
|
|
373
|
+
status: j.status,
|
|
374
|
+
startedAt: j.startedAt.toISOString(),
|
|
375
|
+
completedAt: j.completedAt?.toISOString(),
|
|
376
|
+
success: j.result?.success,
|
|
377
|
+
})),
|
|
378
|
+
}));
|
|
379
|
+
return;
|
|
380
|
+
}
|
|
381
|
+
// =========================================
|
|
382
|
+
// Rules Analyzer Configuration API
|
|
383
|
+
// =========================================
|
|
384
|
+
// Get rules config
|
|
385
|
+
if (path === '/api/rules/config' && req.method === 'GET') {
|
|
386
|
+
try {
|
|
387
|
+
const config = loadRulesConfig();
|
|
388
|
+
res.end(JSON.stringify({
|
|
389
|
+
config,
|
|
390
|
+
examples: EXAMPLE_PATTERNS,
|
|
391
|
+
}));
|
|
392
|
+
}
|
|
393
|
+
catch (e) {
|
|
394
|
+
res.statusCode = 500;
|
|
395
|
+
res.end(JSON.stringify({ error: e instanceof Error ? e.message : 'Failed to load config' }));
|
|
396
|
+
}
|
|
397
|
+
return;
|
|
398
|
+
}
|
|
399
|
+
// Save rules config
|
|
400
|
+
if (path === '/api/rules/config' && req.method === 'PUT') {
|
|
401
|
+
let body = '';
|
|
402
|
+
req.on('data', chunk => body += chunk);
|
|
403
|
+
req.on('end', async () => {
|
|
404
|
+
try {
|
|
405
|
+
const data = JSON.parse(body);
|
|
406
|
+
const validated = RulesAnalyzerConfigSchema.parse(data);
|
|
407
|
+
saveRulesConfig(validated);
|
|
408
|
+
logActivity('query', 'Rules analyzer config updated');
|
|
409
|
+
res.end(JSON.stringify({ success: true, config: validated }));
|
|
410
|
+
}
|
|
411
|
+
catch (e) {
|
|
412
|
+
res.statusCode = 400;
|
|
413
|
+
res.end(JSON.stringify({
|
|
414
|
+
error: e instanceof Error ? e.message : 'Invalid config',
|
|
415
|
+
details: e instanceof Error && 'issues' in e ? e.issues : undefined,
|
|
416
|
+
}));
|
|
417
|
+
}
|
|
418
|
+
});
|
|
419
|
+
return;
|
|
420
|
+
}
|
|
421
|
+
// Validate a regex pattern
|
|
422
|
+
if (path === '/api/rules/validate-pattern' && req.method === 'POST') {
|
|
423
|
+
let body = '';
|
|
424
|
+
req.on('data', chunk => body += chunk);
|
|
425
|
+
req.on('end', () => {
|
|
426
|
+
try {
|
|
427
|
+
const { pattern } = JSON.parse(body);
|
|
428
|
+
const result = validatePattern(pattern);
|
|
429
|
+
res.end(JSON.stringify(result));
|
|
430
|
+
}
|
|
431
|
+
catch (e) {
|
|
432
|
+
res.statusCode = 400;
|
|
433
|
+
res.end(JSON.stringify({ valid: false, error: 'Invalid request' }));
|
|
434
|
+
}
|
|
435
|
+
});
|
|
436
|
+
return;
|
|
437
|
+
}
|
|
438
|
+
// Test a pattern against sample content
|
|
439
|
+
if (path === '/api/rules/test-pattern' && req.method === 'POST') {
|
|
440
|
+
let body = '';
|
|
441
|
+
req.on('data', chunk => body += chunk);
|
|
442
|
+
req.on('end', () => {
|
|
443
|
+
try {
|
|
444
|
+
const { pattern, content } = JSON.parse(body);
|
|
445
|
+
const result = testPattern(pattern, content);
|
|
446
|
+
res.end(JSON.stringify(result));
|
|
447
|
+
}
|
|
448
|
+
catch (e) {
|
|
449
|
+
res.statusCode = 400;
|
|
450
|
+
res.end(JSON.stringify({ matches: false, error: 'Invalid request' }));
|
|
451
|
+
}
|
|
452
|
+
});
|
|
453
|
+
return;
|
|
454
|
+
}
|
|
455
|
+
// Get example patterns (templates)
|
|
456
|
+
if (path === '/api/rules/examples' && req.method === 'GET') {
|
|
457
|
+
res.end(JSON.stringify(EXAMPLE_PATTERNS));
|
|
458
|
+
return;
|
|
459
|
+
}
|
|
460
|
+
// Get available LLM providers
|
|
461
|
+
if (path === '/api/rules/llm/providers' && req.method === 'GET') {
|
|
462
|
+
res.end(JSON.stringify({ providers: LLM_PROVIDERS }));
|
|
463
|
+
return;
|
|
464
|
+
}
|
|
465
|
+
// Test LLM connection and get available models
|
|
466
|
+
if (path === '/api/rules/llm/test' && req.method === 'POST') {
|
|
467
|
+
let body = '';
|
|
468
|
+
req.on('data', chunk => body += chunk);
|
|
469
|
+
req.on('end', async () => {
|
|
470
|
+
try {
|
|
471
|
+
const { provider, apiKey, baseUrl } = JSON.parse(body);
|
|
472
|
+
if (!provider) {
|
|
473
|
+
res.statusCode = 400;
|
|
474
|
+
res.end(JSON.stringify({ success: false, error: 'Provider is required' }));
|
|
475
|
+
return;
|
|
476
|
+
}
|
|
477
|
+
// Build provider config
|
|
478
|
+
const config = {
|
|
479
|
+
provider,
|
|
480
|
+
apiKey,
|
|
481
|
+
baseUrl,
|
|
482
|
+
};
|
|
483
|
+
try {
|
|
484
|
+
const llmProvider = createProvider(config);
|
|
485
|
+
const isAvailable = await llmProvider.isAvailable();
|
|
486
|
+
if (!isAvailable) {
|
|
487
|
+
res.end(JSON.stringify({
|
|
488
|
+
success: false,
|
|
489
|
+
error: provider === 'ollama'
|
|
490
|
+
? 'Ollama is not running. Start it with: ollama serve'
|
|
491
|
+
: 'Invalid API key or provider not available'
|
|
492
|
+
}));
|
|
493
|
+
return;
|
|
494
|
+
}
|
|
495
|
+
// Get available models
|
|
496
|
+
const models = await llmProvider.listModels();
|
|
497
|
+
res.end(JSON.stringify({
|
|
498
|
+
success: true,
|
|
499
|
+
models: models.map(m => ({
|
|
500
|
+
id: m.id,
|
|
501
|
+
name: m.name,
|
|
502
|
+
contextLength: m.capabilities.contextLength,
|
|
503
|
+
supportsJsonMode: m.capabilities.supportsJsonMode,
|
|
504
|
+
}))
|
|
505
|
+
}));
|
|
506
|
+
}
|
|
507
|
+
catch (providerError) {
|
|
508
|
+
res.end(JSON.stringify({
|
|
509
|
+
success: false,
|
|
510
|
+
error: providerError instanceof Error ? providerError.message : 'Connection failed'
|
|
511
|
+
}));
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
catch (e) {
|
|
515
|
+
res.statusCode = 400;
|
|
516
|
+
res.end(JSON.stringify({ success: false, error: 'Invalid request' }));
|
|
517
|
+
}
|
|
518
|
+
});
|
|
519
|
+
return;
|
|
520
|
+
}
|
|
521
|
+
// Save LLM configuration
|
|
522
|
+
if (path === '/api/rules/llm/config' && req.method === 'PUT') {
|
|
523
|
+
let body = '';
|
|
524
|
+
req.on('data', chunk => body += chunk);
|
|
525
|
+
req.on('end', () => {
|
|
526
|
+
try {
|
|
527
|
+
const { provider, apiKey, model, baseUrl } = JSON.parse(body);
|
|
528
|
+
const config = loadRulesConfig();
|
|
529
|
+
config.llm = {
|
|
530
|
+
provider: provider || undefined,
|
|
531
|
+
apiKey: apiKey || undefined,
|
|
532
|
+
model: model || undefined,
|
|
533
|
+
baseUrl: baseUrl || undefined,
|
|
534
|
+
};
|
|
535
|
+
// If LLM is configured, enable useLLM
|
|
536
|
+
if (provider && (apiKey || provider === 'ollama')) {
|
|
537
|
+
config.analysis.useLLM = true;
|
|
538
|
+
}
|
|
539
|
+
saveRulesConfig(config);
|
|
540
|
+
logActivity('query', `LLM provider configured: ${provider}`);
|
|
541
|
+
// Return config without the API key for security
|
|
542
|
+
const safeConfig = { ...config };
|
|
543
|
+
if (safeConfig.llm.apiKey) {
|
|
544
|
+
safeConfig.llm.apiKey = '***configured***';
|
|
545
|
+
}
|
|
546
|
+
res.end(JSON.stringify({ success: true, config: safeConfig }));
|
|
547
|
+
}
|
|
548
|
+
catch (e) {
|
|
549
|
+
res.statusCode = 400;
|
|
550
|
+
res.end(JSON.stringify({ error: e instanceof Error ? e.message : 'Invalid request' }));
|
|
551
|
+
}
|
|
552
|
+
});
|
|
553
|
+
return;
|
|
554
|
+
}
|
|
555
|
+
// Get LLM configuration (without exposing API key)
|
|
556
|
+
if (path === '/api/rules/llm/config' && req.method === 'GET') {
|
|
557
|
+
try {
|
|
558
|
+
const config = loadRulesConfig();
|
|
559
|
+
const safeConfig = {
|
|
560
|
+
provider: config.llm?.provider,
|
|
561
|
+
model: config.llm?.model,
|
|
562
|
+
baseUrl: config.llm?.baseUrl,
|
|
563
|
+
hasApiKey: !!config.llm?.apiKey,
|
|
564
|
+
};
|
|
565
|
+
res.end(JSON.stringify(safeConfig));
|
|
566
|
+
}
|
|
567
|
+
catch (e) {
|
|
568
|
+
res.statusCode = 500;
|
|
569
|
+
res.end(JSON.stringify({ error: 'Failed to load config' }));
|
|
570
|
+
}
|
|
571
|
+
return;
|
|
572
|
+
}
|
|
573
|
+
// Add a version check pattern
|
|
574
|
+
if (path === '/api/rules/config/version-checks' && req.method === 'POST') {
|
|
575
|
+
let body = '';
|
|
576
|
+
req.on('data', chunk => body += chunk);
|
|
577
|
+
req.on('end', () => {
|
|
578
|
+
try {
|
|
579
|
+
const newCheck = JSON.parse(body);
|
|
580
|
+
const config = loadRulesConfig();
|
|
581
|
+
// Validate the pattern
|
|
582
|
+
const validation = validatePattern(newCheck.pattern);
|
|
583
|
+
if (!validation.valid) {
|
|
584
|
+
res.statusCode = 400;
|
|
585
|
+
res.end(JSON.stringify({ error: `Invalid pattern: ${validation.error}` }));
|
|
586
|
+
return;
|
|
587
|
+
}
|
|
588
|
+
config.versionChecks.push({
|
|
589
|
+
...newCheck,
|
|
590
|
+
enabled: newCheck.enabled ?? true,
|
|
591
|
+
});
|
|
592
|
+
saveRulesConfig(config);
|
|
593
|
+
res.end(JSON.stringify({ success: true, config }));
|
|
594
|
+
}
|
|
595
|
+
catch (e) {
|
|
596
|
+
res.statusCode = 400;
|
|
597
|
+
res.end(JSON.stringify({ error: e instanceof Error ? e.message : 'Invalid request' }));
|
|
598
|
+
}
|
|
599
|
+
});
|
|
600
|
+
return;
|
|
601
|
+
}
|
|
602
|
+
// Add a deprecation pattern
|
|
603
|
+
if (path === '/api/rules/config/deprecation-patterns' && req.method === 'POST') {
|
|
604
|
+
let body = '';
|
|
605
|
+
req.on('data', chunk => body += chunk);
|
|
606
|
+
req.on('end', () => {
|
|
607
|
+
try {
|
|
608
|
+
const newPattern = JSON.parse(body);
|
|
609
|
+
const config = loadRulesConfig();
|
|
610
|
+
// Validate the pattern
|
|
611
|
+
const validation = validatePattern(newPattern.pattern);
|
|
612
|
+
if (!validation.valid) {
|
|
613
|
+
res.statusCode = 400;
|
|
614
|
+
res.end(JSON.stringify({ error: `Invalid pattern: ${validation.error}` }));
|
|
615
|
+
return;
|
|
616
|
+
}
|
|
617
|
+
config.deprecationPatterns.push({
|
|
618
|
+
...newPattern,
|
|
619
|
+
enabled: newPattern.enabled ?? true,
|
|
620
|
+
});
|
|
621
|
+
saveRulesConfig(config);
|
|
622
|
+
res.end(JSON.stringify({ success: true, config }));
|
|
623
|
+
}
|
|
624
|
+
catch (e) {
|
|
625
|
+
res.statusCode = 400;
|
|
626
|
+
res.end(JSON.stringify({ error: e instanceof Error ? e.message : 'Invalid request' }));
|
|
627
|
+
}
|
|
628
|
+
});
|
|
629
|
+
return;
|
|
630
|
+
}
|
|
631
|
+
// Delete a version check or deprecation pattern by index
|
|
632
|
+
const deletePatternMatch = path.match(/^\/api\/rules\/config\/(version-checks|deprecation-patterns)\/(\d+)$/);
|
|
633
|
+
if (deletePatternMatch && req.method === 'DELETE') {
|
|
634
|
+
const [, patternType, indexStr] = deletePatternMatch;
|
|
635
|
+
const index = parseInt(indexStr, 10);
|
|
636
|
+
try {
|
|
637
|
+
const config = loadRulesConfig();
|
|
638
|
+
const array = patternType === 'version-checks'
|
|
639
|
+
? config.versionChecks
|
|
640
|
+
: config.deprecationPatterns;
|
|
641
|
+
if (index < 0 || index >= array.length) {
|
|
642
|
+
res.statusCode = 404;
|
|
643
|
+
res.end(JSON.stringify({ error: 'Pattern not found' }));
|
|
644
|
+
return;
|
|
645
|
+
}
|
|
646
|
+
array.splice(index, 1);
|
|
647
|
+
saveRulesConfig(config);
|
|
648
|
+
res.end(JSON.stringify({ success: true, config }));
|
|
649
|
+
}
|
|
650
|
+
catch (e) {
|
|
651
|
+
res.statusCode = 500;
|
|
652
|
+
res.end(JSON.stringify({ error: e instanceof Error ? e.message : 'Failed to delete' }));
|
|
653
|
+
}
|
|
654
|
+
return;
|
|
655
|
+
}
|
|
656
|
+
// Toggle pattern enabled/disabled
|
|
657
|
+
const togglePatternMatch = path.match(/^\/api\/rules\/config\/(version-checks|deprecation-patterns)\/(\d+)\/toggle$/);
|
|
658
|
+
if (togglePatternMatch && req.method === 'POST') {
|
|
659
|
+
const [, patternType, indexStr] = togglePatternMatch;
|
|
660
|
+
const index = parseInt(indexStr, 10);
|
|
661
|
+
try {
|
|
662
|
+
const config = loadRulesConfig();
|
|
663
|
+
const array = patternType === 'version-checks'
|
|
664
|
+
? config.versionChecks
|
|
665
|
+
: config.deprecationPatterns;
|
|
666
|
+
if (index < 0 || index >= array.length) {
|
|
667
|
+
res.statusCode = 404;
|
|
668
|
+
res.end(JSON.stringify({ error: 'Pattern not found' }));
|
|
669
|
+
return;
|
|
670
|
+
}
|
|
671
|
+
array[index].enabled = !array[index].enabled;
|
|
672
|
+
saveRulesConfig(config);
|
|
673
|
+
res.end(JSON.stringify({ success: true, enabled: array[index].enabled, config }));
|
|
674
|
+
}
|
|
675
|
+
catch (e) {
|
|
676
|
+
res.statusCode = 500;
|
|
677
|
+
res.end(JSON.stringify({ error: e instanceof Error ? e.message : 'Failed to toggle' }));
|
|
678
|
+
}
|
|
679
|
+
return;
|
|
680
|
+
}
|
|
681
|
+
// Get system home directory
|
|
682
|
+
if (path === '/api/system/home' && req.method === 'GET') {
|
|
683
|
+
const home = process.env.HOME || process.env.USERPROFILE || '~';
|
|
684
|
+
res.end(JSON.stringify({ home }));
|
|
685
|
+
return;
|
|
686
|
+
}
|
|
687
|
+
// List directories (for folder browsing)
|
|
688
|
+
if (path === '/api/system/browse' && req.method === 'POST') {
|
|
689
|
+
let body = '';
|
|
690
|
+
req.on('data', chunk => body += chunk);
|
|
691
|
+
req.on('end', async () => {
|
|
692
|
+
try {
|
|
693
|
+
const { directory, showHidden = true } = JSON.parse(body);
|
|
694
|
+
const { readdirSync, statSync, existsSync } = await import('fs');
|
|
695
|
+
const { join, resolve, dirname } = await import('path');
|
|
696
|
+
// Expand ~ to home directory
|
|
697
|
+
const home = process.env.HOME || process.env.USERPROFILE || '';
|
|
698
|
+
let expandedDir = directory.replace(/^~/, home);
|
|
699
|
+
// Resolve to absolute path
|
|
700
|
+
expandedDir = resolve(expandedDir);
|
|
701
|
+
// Check if directory exists
|
|
702
|
+
if (!existsSync(expandedDir)) {
|
|
703
|
+
res.statusCode = 400;
|
|
704
|
+
res.end(JSON.stringify({ error: `Directory not found: ${expandedDir}` }));
|
|
705
|
+
return;
|
|
706
|
+
}
|
|
707
|
+
const entries = readdirSync(expandedDir, { withFileTypes: true });
|
|
708
|
+
const folders = entries
|
|
709
|
+
.filter(e => {
|
|
710
|
+
if (!e.isDirectory())
|
|
711
|
+
return false;
|
|
712
|
+
// Show important hidden folders like .cursor, .codex, .config
|
|
713
|
+
if (e.name.startsWith('.')) {
|
|
714
|
+
const importantHidden = ['.cursor', '.codex', '.config', '.local', '.npm', '.vscode', '.git'];
|
|
715
|
+
return showHidden && importantHidden.some(h => e.name === h || e.name.startsWith(h + '-'));
|
|
716
|
+
}
|
|
717
|
+
return true;
|
|
718
|
+
})
|
|
719
|
+
.map(e => ({
|
|
720
|
+
name: e.name,
|
|
721
|
+
path: join(expandedDir, e.name),
|
|
722
|
+
}))
|
|
723
|
+
.sort((a, b) => {
|
|
724
|
+
// Sort: hidden folders first, then alphabetically
|
|
725
|
+
const aHidden = a.name.startsWith('.');
|
|
726
|
+
const bHidden = b.name.startsWith('.');
|
|
727
|
+
if (aHidden && !bHidden)
|
|
728
|
+
return -1;
|
|
729
|
+
if (!aHidden && bHidden)
|
|
730
|
+
return 1;
|
|
731
|
+
return a.name.localeCompare(b.name);
|
|
732
|
+
})
|
|
733
|
+
.slice(0, 100); // Limit results
|
|
734
|
+
const parent = dirname(expandedDir);
|
|
735
|
+
res.end(JSON.stringify({
|
|
736
|
+
current: expandedDir,
|
|
737
|
+
folders,
|
|
738
|
+
parent: parent !== expandedDir ? parent : '', // Empty if at root
|
|
739
|
+
}));
|
|
740
|
+
}
|
|
741
|
+
catch (e) {
|
|
742
|
+
res.statusCode = 400;
|
|
743
|
+
res.end(JSON.stringify({ error: e instanceof Error ? e.message : 'Failed to browse' }));
|
|
744
|
+
}
|
|
745
|
+
});
|
|
746
|
+
return;
|
|
747
|
+
}
|
|
748
|
+
// ==================== RULES OPTIMIZER ENDPOINTS ====================
|
|
749
|
+
// Analyze rules in a folder (step 1)
|
|
750
|
+
if (path === '/api/rules/analyze' && req.method === 'POST') {
|
|
751
|
+
let body = '';
|
|
752
|
+
req.on('data', chunk => body += chunk);
|
|
753
|
+
req.on('end', async () => {
|
|
754
|
+
try {
|
|
755
|
+
const { folder } = JSON.parse(body);
|
|
756
|
+
if (!folder) {
|
|
757
|
+
res.statusCode = 400;
|
|
758
|
+
res.end(JSON.stringify({ error: 'folder is required' }));
|
|
759
|
+
return;
|
|
760
|
+
}
|
|
761
|
+
const ragConfig = loadConfig();
|
|
762
|
+
const parser = new RulesParser();
|
|
763
|
+
const analyzer = getRulesAnalyzer(ragConfig);
|
|
764
|
+
const rules = parser.parseDirectory(folder);
|
|
765
|
+
const duplicates = await analyzer.findDuplicates(rules);
|
|
766
|
+
const conflicts = await analyzer.findConflicts(rules);
|
|
767
|
+
const outdated = analyzer.findOutdatedRules(rules);
|
|
768
|
+
res.end(JSON.stringify({
|
|
769
|
+
success: true,
|
|
770
|
+
folder,
|
|
771
|
+
stats: {
|
|
772
|
+
totalRules: rules.length,
|
|
773
|
+
duplicates: duplicates.length,
|
|
774
|
+
conflicts: conflicts.length,
|
|
775
|
+
outdated: outdated.length,
|
|
776
|
+
},
|
|
777
|
+
rules: rules.map((r) => ({
|
|
778
|
+
title: r.title,
|
|
779
|
+
path: r.sourceFile.path,
|
|
780
|
+
tokens: r.tokenCount,
|
|
781
|
+
tags: r.tags,
|
|
782
|
+
})),
|
|
783
|
+
duplicates: duplicates.map(d => ({
|
|
784
|
+
rule1: { title: d.rule1.title, path: d.rule1.sourceFile.path },
|
|
785
|
+
rule2: { title: d.rule2.title, path: d.rule2.sourceFile.path },
|
|
786
|
+
similarity: d.similarity,
|
|
787
|
+
matchType: d.matchType,
|
|
788
|
+
})),
|
|
789
|
+
conflicts,
|
|
790
|
+
outdated,
|
|
791
|
+
}));
|
|
792
|
+
}
|
|
793
|
+
catch (e) {
|
|
794
|
+
res.statusCode = 500;
|
|
795
|
+
res.end(JSON.stringify({ error: e instanceof Error ? e.message : 'Analysis failed' }));
|
|
796
|
+
}
|
|
797
|
+
});
|
|
798
|
+
return;
|
|
799
|
+
}
|
|
800
|
+
// Auto-optimize rules (analyze + merge + cleanup in one step)
|
|
801
|
+
if (path === '/api/rules/auto-optimize' && req.method === 'POST') {
|
|
802
|
+
let body = '';
|
|
803
|
+
req.on('data', chunk => body += chunk);
|
|
804
|
+
req.on('end', async () => {
|
|
805
|
+
try {
|
|
806
|
+
const { folder, dryRun = true } = JSON.parse(body);
|
|
807
|
+
if (!folder) {
|
|
808
|
+
res.statusCode = 400;
|
|
809
|
+
res.end(JSON.stringify({ error: 'folder is required' }));
|
|
810
|
+
return;
|
|
811
|
+
}
|
|
812
|
+
const config = loadRulesConfig();
|
|
813
|
+
const useLLM = config.analysis.useLLM && config.llm.provider && config.llm.apiKey;
|
|
814
|
+
// If LLM is enabled but not configured, return error
|
|
815
|
+
if (config.analysis.useLLM && (!config.llm.provider || !config.llm.apiKey)) {
|
|
816
|
+
res.statusCode = 400;
|
|
817
|
+
res.end(JSON.stringify({
|
|
818
|
+
error: 'LLM is enabled but not configured. Either disable "Use LLM for Analysis" in Settings, or configure an LLM provider.'
|
|
819
|
+
}));
|
|
820
|
+
return;
|
|
821
|
+
}
|
|
822
|
+
const ragConfig = loadConfig();
|
|
823
|
+
const parser = new RulesParser();
|
|
824
|
+
const analyzer = getRulesAnalyzer(ragConfig);
|
|
825
|
+
// Step 1: Parse all rules
|
|
826
|
+
const rules = parser.parseDirectory(folder);
|
|
827
|
+
if (rules.length === 0) {
|
|
828
|
+
res.end(JSON.stringify({
|
|
829
|
+
success: true,
|
|
830
|
+
dryRun,
|
|
831
|
+
message: 'No rules found in the specified folder',
|
|
832
|
+
stats: { totalRules: 0, duplicates: 0, conflicts: 0, outdated: 0, merged: 0, deleted: 0 },
|
|
833
|
+
actions: [],
|
|
834
|
+
}));
|
|
835
|
+
return;
|
|
836
|
+
}
|
|
837
|
+
// Step 2: Find issues using pattern matching (always works, no LLM needed)
|
|
838
|
+
const duplicates = await analyzer.findDuplicates(rules);
|
|
839
|
+
const conflicts = await analyzer.findConflicts(rules);
|
|
840
|
+
const outdated = analyzer.findOutdatedRules(rules);
|
|
841
|
+
const actions = [];
|
|
842
|
+
let merged = 0;
|
|
843
|
+
let deleted = 0;
|
|
844
|
+
// Add outdated rule warnings
|
|
845
|
+
for (const issue of outdated) {
|
|
846
|
+
actions.push({
|
|
847
|
+
type: 'outdated',
|
|
848
|
+
path: issue.rule.sourceFile.path,
|
|
849
|
+
reason: issue.reason,
|
|
850
|
+
severity: 'warning',
|
|
851
|
+
});
|
|
852
|
+
}
|
|
853
|
+
// Add conflict warnings
|
|
854
|
+
for (const conflict of conflicts) {
|
|
855
|
+
actions.push({
|
|
856
|
+
type: 'warning',
|
|
857
|
+
path: conflict.rule1.sourceFile.path,
|
|
858
|
+
reason: `Conflicts with ${conflict.rule2.title}: ${conflict.description}`,
|
|
859
|
+
severity: 'error',
|
|
860
|
+
});
|
|
861
|
+
}
|
|
862
|
+
// Step 3: Handle duplicates
|
|
863
|
+
if (duplicates.length > 0) {
|
|
864
|
+
if (useLLM) {
|
|
865
|
+
// Use LLM to intelligently merge duplicates
|
|
866
|
+
const merger = getRulesMerger();
|
|
867
|
+
// Group duplicates into clusters for merging
|
|
868
|
+
const processed = new Set();
|
|
869
|
+
const clusters = [];
|
|
870
|
+
for (const dup of duplicates) {
|
|
871
|
+
const path1 = dup.rule1.sourceFile.path;
|
|
872
|
+
const path2 = dup.rule2.sourceFile.path;
|
|
873
|
+
if (processed.has(path1) && processed.has(path2))
|
|
874
|
+
continue;
|
|
875
|
+
let cluster = clusters.find(c => c.rules.some((r) => r.sourceFile.path === path1 || r.sourceFile.path === path2));
|
|
876
|
+
if (!cluster) {
|
|
877
|
+
cluster = { rules: [], similarity: dup.similarity };
|
|
878
|
+
clusters.push(cluster);
|
|
879
|
+
}
|
|
880
|
+
if (!cluster.rules.some((r) => r.sourceFile.path === path1)) {
|
|
881
|
+
const rule = rules.find((r) => r.sourceFile.path === path1);
|
|
882
|
+
if (rule)
|
|
883
|
+
cluster.rules.push(rule);
|
|
884
|
+
}
|
|
885
|
+
if (!cluster.rules.some((r) => r.sourceFile.path === path2)) {
|
|
886
|
+
const rule = rules.find((r) => r.sourceFile.path === path2);
|
|
887
|
+
if (rule)
|
|
888
|
+
cluster.rules.push(rule);
|
|
889
|
+
}
|
|
890
|
+
processed.add(path1);
|
|
891
|
+
processed.add(path2);
|
|
892
|
+
}
|
|
893
|
+
// Merge each cluster using LLM
|
|
894
|
+
for (const cluster of clusters) {
|
|
895
|
+
if (cluster.rules.length < 2)
|
|
896
|
+
continue;
|
|
897
|
+
try {
|
|
898
|
+
const mergeResult = await merger.mergeRules(cluster.rules, {
|
|
899
|
+
context: `These rules have ${Math.round(cluster.similarity * 100)}% similarity.`,
|
|
900
|
+
});
|
|
901
|
+
if (mergeResult.success && mergeResult.mergedContent) {
|
|
902
|
+
const keepPath = cluster.rules[0].sourceFile.path;
|
|
903
|
+
const deletePaths = cluster.rules.slice(1).map((r) => r.sourceFile.path);
|
|
904
|
+
actions.push({
|
|
905
|
+
type: 'merge',
|
|
906
|
+
path: keepPath,
|
|
907
|
+
reason: `Merged ${cluster.rules.length} similar rules (${Math.round(cluster.similarity * 100)}% similarity)`,
|
|
908
|
+
content: mergeResult.mergedContent,
|
|
909
|
+
});
|
|
910
|
+
for (const delPath of deletePaths) {
|
|
911
|
+
actions.push({
|
|
912
|
+
type: 'delete',
|
|
913
|
+
path: delPath,
|
|
914
|
+
reason: `Content merged into ${keepPath}`,
|
|
915
|
+
});
|
|
916
|
+
}
|
|
917
|
+
merged += cluster.rules.length;
|
|
918
|
+
deleted += deletePaths.length;
|
|
919
|
+
}
|
|
920
|
+
}
|
|
921
|
+
catch (mergeError) {
|
|
922
|
+
console.error(`Failed to merge cluster:`, mergeError);
|
|
923
|
+
}
|
|
924
|
+
}
|
|
925
|
+
}
|
|
926
|
+
else {
|
|
927
|
+
// Without LLM: just report duplicates as warnings
|
|
928
|
+
for (const dup of duplicates) {
|
|
929
|
+
actions.push({
|
|
930
|
+
type: 'warning',
|
|
931
|
+
path: dup.rule1.sourceFile.path,
|
|
932
|
+
reason: `Duplicate of "${dup.rule2.title}" (${Math.round(dup.similarity * 100)}% similar) - enable LLM to auto-merge`,
|
|
933
|
+
severity: 'warning',
|
|
934
|
+
});
|
|
935
|
+
}
|
|
936
|
+
}
|
|
937
|
+
}
|
|
938
|
+
// Step 4: Apply changes if not dry run and LLM was used for merging
|
|
939
|
+
if (!dryRun && useLLM && actions.some(a => a.type === 'merge' || a.type === 'delete')) {
|
|
940
|
+
const backupFolder = join(folder, '.rules-backup-' + Date.now());
|
|
941
|
+
mkdirSync(backupFolder, { recursive: true });
|
|
942
|
+
for (const action of actions) {
|
|
943
|
+
try {
|
|
944
|
+
if (action.type === 'merge' && action.content) {
|
|
945
|
+
const filename = action.path.split('/').pop() || 'rule';
|
|
946
|
+
copyFileSync(action.path, join(backupFolder, filename));
|
|
947
|
+
writeFileSync(action.path, action.content, 'utf-8');
|
|
948
|
+
}
|
|
949
|
+
else if (action.type === 'delete') {
|
|
950
|
+
const filename = action.path.split('/').pop() || 'rule';
|
|
951
|
+
copyFileSync(action.path, join(backupFolder, filename));
|
|
952
|
+
unlinkSync(action.path);
|
|
953
|
+
}
|
|
954
|
+
}
|
|
955
|
+
catch (fileError) {
|
|
956
|
+
console.error(`Failed to apply action for ${action.path}:`, fileError);
|
|
957
|
+
}
|
|
958
|
+
}
|
|
959
|
+
}
|
|
960
|
+
const totalIssues = duplicates.length + conflicts.length + outdated.length;
|
|
961
|
+
let message;
|
|
962
|
+
if (totalIssues === 0) {
|
|
963
|
+
message = `Analyzed ${rules.length} rules - no issues found!`;
|
|
964
|
+
}
|
|
965
|
+
else if (useLLM && !dryRun) {
|
|
966
|
+
message = `Applied ${actions.filter(a => a.type === 'merge' || a.type === 'delete').length} optimizations`;
|
|
967
|
+
}
|
|
968
|
+
else if (useLLM && dryRun) {
|
|
969
|
+
message = `Found ${totalIssues} issues (dry run - no changes made)`;
|
|
970
|
+
}
|
|
971
|
+
else {
|
|
972
|
+
message = `Found ${totalIssues} issues. Enable LLM to auto-merge duplicates.`;
|
|
973
|
+
}
|
|
974
|
+
res.end(JSON.stringify({
|
|
975
|
+
success: true,
|
|
976
|
+
dryRun,
|
|
977
|
+
usedLLM: useLLM,
|
|
978
|
+
message,
|
|
979
|
+
stats: {
|
|
980
|
+
totalRules: rules.length,
|
|
981
|
+
duplicates: duplicates.length,
|
|
982
|
+
conflicts: conflicts.length,
|
|
983
|
+
outdated: outdated.length,
|
|
984
|
+
merged,
|
|
985
|
+
deleted,
|
|
986
|
+
},
|
|
987
|
+
actions: actions.map(a => ({
|
|
988
|
+
type: a.type,
|
|
989
|
+
path: a.path,
|
|
990
|
+
reason: a.reason,
|
|
991
|
+
severity: a.severity,
|
|
992
|
+
hasContent: !!a.content,
|
|
993
|
+
})),
|
|
994
|
+
}));
|
|
995
|
+
}
|
|
996
|
+
catch (e) {
|
|
997
|
+
res.statusCode = 500;
|
|
998
|
+
res.end(JSON.stringify({ error: e instanceof Error ? e.message : 'Auto-optimize failed' }));
|
|
999
|
+
}
|
|
1000
|
+
});
|
|
1001
|
+
return;
|
|
1002
|
+
}
|
|
1003
|
+
// Preview a merge (without applying)
|
|
1004
|
+
if (path === '/api/rules/preview-merge' && req.method === 'POST') {
|
|
1005
|
+
let body = '';
|
|
1006
|
+
req.on('data', chunk => body += chunk);
|
|
1007
|
+
req.on('end', async () => {
|
|
1008
|
+
try {
|
|
1009
|
+
const { paths } = JSON.parse(body);
|
|
1010
|
+
if (!paths || !Array.isArray(paths) || paths.length < 2) {
|
|
1011
|
+
res.statusCode = 400;
|
|
1012
|
+
res.end(JSON.stringify({ error: 'At least 2 rule paths are required' }));
|
|
1013
|
+
return;
|
|
1014
|
+
}
|
|
1015
|
+
const config = loadRulesConfig();
|
|
1016
|
+
if (!config.llm.provider || !config.llm.apiKey) {
|
|
1017
|
+
res.statusCode = 400;
|
|
1018
|
+
res.end(JSON.stringify({ error: 'LLM not configured' }));
|
|
1019
|
+
return;
|
|
1020
|
+
}
|
|
1021
|
+
const parser = new RulesParser();
|
|
1022
|
+
const merger = getRulesMerger();
|
|
1023
|
+
// Parse the specific rules
|
|
1024
|
+
const rules = [];
|
|
1025
|
+
for (const p of paths) {
|
|
1026
|
+
const ruleFile = parser.readRuleFile(p);
|
|
1027
|
+
if (ruleFile) {
|
|
1028
|
+
const parsed = parser.parseFile(ruleFile);
|
|
1029
|
+
rules.push(...parsed);
|
|
1030
|
+
}
|
|
1031
|
+
}
|
|
1032
|
+
if (rules.length < 2) {
|
|
1033
|
+
res.statusCode = 400;
|
|
1034
|
+
res.end(JSON.stringify({ error: 'Could not parse enough rules' }));
|
|
1035
|
+
return;
|
|
1036
|
+
}
|
|
1037
|
+
const result = await merger.mergeRules(rules);
|
|
1038
|
+
res.end(JSON.stringify({
|
|
1039
|
+
success: result.success,
|
|
1040
|
+
mergedContent: result.mergedContent,
|
|
1041
|
+
mergedTitle: result.mergedTitle,
|
|
1042
|
+
originalRules: rules.map((r) => ({ title: r.title, path: r.sourceFile.path, tokens: r.tokenCount })),
|
|
1043
|
+
}));
|
|
1044
|
+
}
|
|
1045
|
+
catch (e) {
|
|
1046
|
+
res.statusCode = 500;
|
|
1047
|
+
res.end(JSON.stringify({ error: e instanceof Error ? e.message : 'Preview failed' }));
|
|
1048
|
+
}
|
|
1049
|
+
});
|
|
1050
|
+
return;
|
|
1051
|
+
}
|
|
1052
|
+
// ==================== END RULES OPTIMIZER ENDPOINTS ====================
|
|
222
1053
|
res.statusCode = 404;
|
|
223
1054
|
res.end(JSON.stringify({ error: 'Not found' }));
|
|
224
1055
|
}
|
|
@@ -368,6 +1199,8 @@ function serveStatic(res, filePath) {
|
|
|
368
1199
|
res.end(readFileSync(fullPath));
|
|
369
1200
|
}
|
|
370
1201
|
export function startDashboard(port = 3333) {
|
|
1202
|
+
// Register core tools when dashboard starts
|
|
1203
|
+
registerCoreTools();
|
|
371
1204
|
const server = createServer(async (req, res) => {
|
|
372
1205
|
const url = new URL(req.url || '/', `http://localhost:${port}`);
|
|
373
1206
|
const path = url.pathname;
|