@syke1/mcp-server 1.4.16 → 1.4.17
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/ai/realtime-analyzer.js +66 -1
- package/dist/watcher/file-cache.js +1 -1
- package/dist/web/server.js +23 -2
- package/package.json +2 -2
|
@@ -35,9 +35,42 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
35
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
36
|
exports.analyzeChangeRealtime = analyzeChangeRealtime;
|
|
37
37
|
const analyze_impact_1 = require("../tools/analyze-impact");
|
|
38
|
+
const crypto = __importStar(require("crypto"));
|
|
38
39
|
const path = __importStar(require("path"));
|
|
39
40
|
const provider_1 = require("./provider");
|
|
40
41
|
const context_extractor_1 = require("./context-extractor");
|
|
42
|
+
const analysisCache = new Map();
|
|
43
|
+
const MAX_CACHE_SIZE = 100;
|
|
44
|
+
function computeContentHash(content, diff) {
|
|
45
|
+
return crypto.createHash("md5").update((content || "") + "\n---\n" + diff).digest("hex");
|
|
46
|
+
}
|
|
47
|
+
function evictOldestCacheEntry() {
|
|
48
|
+
let oldestKey = null;
|
|
49
|
+
let oldestTime = Infinity;
|
|
50
|
+
for (const [key, entry] of analysisCache) {
|
|
51
|
+
if (entry.insertedAt < oldestTime) {
|
|
52
|
+
oldestTime = entry.insertedAt;
|
|
53
|
+
oldestKey = key;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
if (oldestKey)
|
|
57
|
+
analysisCache.delete(oldestKey);
|
|
58
|
+
}
|
|
59
|
+
// ── Rate limiter: max 10 AI calls per minute (sliding window) ──
|
|
60
|
+
const RATE_LIMIT_MAX = 10;
|
|
61
|
+
const RATE_LIMIT_WINDOW_MS = 60000;
|
|
62
|
+
const callTimestamps = [];
|
|
63
|
+
function isRateLimited() {
|
|
64
|
+
const now = Date.now();
|
|
65
|
+
// Remove timestamps outside the window
|
|
66
|
+
while (callTimestamps.length > 0 && callTimestamps[0] <= now - RATE_LIMIT_WINDOW_MS) {
|
|
67
|
+
callTimestamps.shift();
|
|
68
|
+
}
|
|
69
|
+
return callTimestamps.length >= RATE_LIMIT_MAX;
|
|
70
|
+
}
|
|
71
|
+
function recordCall() {
|
|
72
|
+
callTimestamps.push(Date.now());
|
|
73
|
+
}
|
|
41
74
|
function getSystemPrompt() {
|
|
42
75
|
return `You are a senior full-stack architect and code impact monitoring AI with 20 years of experience.
|
|
43
76
|
Role: Detect potential errors and cascading impacts before build when files are modified/added/deleted.
|
|
@@ -147,14 +180,41 @@ ${diffSummary}
|
|
|
147
180
|
${connectedFiles.length > 0 ? `## Connected files (${connectedFiles.length})\n${connectedFiles.join("\n\n")}` : "No connected files"}
|
|
148
181
|
|
|
149
182
|
Analyze the impact of this change on the project.`;
|
|
183
|
+
// ── Hash cache check: skip AI if content+diff unchanged ──
|
|
184
|
+
const diffStr = change.diff.map(d => `${d.type}:${d.line}:${d.old || ""}:${d.new || ""}`).join("|");
|
|
185
|
+
const contentHash = computeContentHash(change.newContent, diffStr);
|
|
186
|
+
const cached = analysisCache.get(relPath);
|
|
187
|
+
if (cached && cached.hash === contentHash) {
|
|
188
|
+
console.error(`[syke:ai] Cache hit for ${relPath} — skipping AI call`);
|
|
189
|
+
return { ...cached.result, timestamp: change.timestamp, analysisMs: 0 };
|
|
190
|
+
}
|
|
191
|
+
// ── Rate limit check ──
|
|
192
|
+
if (isRateLimited()) {
|
|
193
|
+
const analysisMs = Date.now() - start;
|
|
194
|
+
console.error(`[syke:ai] Rate limit reached (${RATE_LIMIT_MAX}/min) — skipping AI for ${relPath}`);
|
|
195
|
+
return {
|
|
196
|
+
file: relPath,
|
|
197
|
+
changeType: change.type,
|
|
198
|
+
timestamp: change.timestamp,
|
|
199
|
+
riskLevel: affectedNodes.length >= 10 ? "HIGH" : affectedNodes.length >= 5 ? "MEDIUM" : "LOW",
|
|
200
|
+
summary: `Rate limited — graph-based analysis: ${affectedNodes.length} files impacted`,
|
|
201
|
+
brokenImports: [],
|
|
202
|
+
sideEffects: [],
|
|
203
|
+
warnings: ["AI analysis skipped: rate limit (10 calls/min)"],
|
|
204
|
+
suggestion: "Wait a moment for AI analysis to resume",
|
|
205
|
+
affectedNodes,
|
|
206
|
+
analysisMs,
|
|
207
|
+
};
|
|
208
|
+
}
|
|
150
209
|
try {
|
|
151
210
|
const provider = (0, provider_1.getAIProvider)();
|
|
152
211
|
if (!provider) {
|
|
153
212
|
throw new Error("No AI provider available (set GEMINI_KEY, OPENAI_KEY, or ANTHROPIC_KEY)");
|
|
154
213
|
}
|
|
214
|
+
recordCall();
|
|
155
215
|
const parsed = await provider.analyzeJSON(getSystemPrompt(), userPrompt);
|
|
156
216
|
const analysisMs = Date.now() - start;
|
|
157
|
-
|
|
217
|
+
const result = {
|
|
158
218
|
file: relPath,
|
|
159
219
|
changeType: change.type,
|
|
160
220
|
timestamp: change.timestamp,
|
|
@@ -167,6 +227,11 @@ Analyze the impact of this change on the project.`;
|
|
|
167
227
|
affectedNodes,
|
|
168
228
|
analysisMs,
|
|
169
229
|
};
|
|
230
|
+
// Store in cache
|
|
231
|
+
if (analysisCache.size >= MAX_CACHE_SIZE)
|
|
232
|
+
evictOldestCacheEntry();
|
|
233
|
+
analysisCache.set(relPath, { hash: contentHash, result, insertedAt: Date.now() });
|
|
234
|
+
return result;
|
|
170
235
|
}
|
|
171
236
|
catch (err) {
|
|
172
237
|
const analysisMs = Date.now() - start;
|
|
@@ -50,7 +50,7 @@ class FileCache extends events_1.EventEmitter {
|
|
|
50
50
|
this.sourceDirs = [];
|
|
51
51
|
this.watcher = null;
|
|
52
52
|
this.debounceTimers = new Map();
|
|
53
|
-
this.DEBOUNCE_MS =
|
|
53
|
+
this.DEBOUNCE_MS = 1500;
|
|
54
54
|
this.plugins = (0, plugin_1.detectLanguages)(projectRoot);
|
|
55
55
|
// Collect all extensions from detected plugins
|
|
56
56
|
const allExts = new Set();
|
package/dist/web/server.js
CHANGED
|
@@ -49,6 +49,8 @@ const analyze_impact_1 = require("../tools/analyze-impact");
|
|
|
49
49
|
const analyzer_1 = require("../ai/analyzer");
|
|
50
50
|
const realtime_analyzer_1 = require("../ai/realtime-analyzer");
|
|
51
51
|
const config_1 = require("../config");
|
|
52
|
+
// ── Real-time AI analysis toggle ──
|
|
53
|
+
let realtimeAIEnabled = true;
|
|
52
54
|
function resolveFilePath(fileArg, projectRoot, sourceDir) {
|
|
53
55
|
const srcDir = sourceDir || path.join(projectRoot, "lib");
|
|
54
56
|
const srcDirName = path.basename(srcDir); // "lib" or "src"
|
|
@@ -318,8 +320,8 @@ function createWebServer(getGraphFn, initialFileCache, switchProjectFn, getProje
|
|
|
318
320
|
connectedNodes,
|
|
319
321
|
timestamp: change.timestamp,
|
|
320
322
|
});
|
|
321
|
-
// Run Gemini real-time analysis (Pro only)
|
|
322
|
-
if (isProPlan()) {
|
|
323
|
+
// Run Gemini real-time analysis (Pro only, when toggle is on)
|
|
324
|
+
if (isProPlan() && realtimeAIEnabled) {
|
|
323
325
|
broadcastSSE("analysis-start", { file: change.relativePath });
|
|
324
326
|
try {
|
|
325
327
|
const analysis = await (0, realtime_analyzer_1.analyzeChangeRealtime)(change, graph, (relPath) => currentFileCache?.getFileByRelPath(relPath) ?? null);
|
|
@@ -338,6 +340,13 @@ function createWebServer(getGraphFn, initialFileCache, switchProjectFn, getProje
|
|
|
338
340
|
});
|
|
339
341
|
}
|
|
340
342
|
}
|
|
343
|
+
else if (isProPlan() && !realtimeAIEnabled) {
|
|
344
|
+
// Pro but AI toggle is off — log and skip AI, still handle structural changes
|
|
345
|
+
console.error(`[syke:ai] Real-time AI disabled — skipping analysis for ${change.relativePath}`);
|
|
346
|
+
if (change.type === "added" || change.type === "deleted") {
|
|
347
|
+
broadcastSSE("graph-rebuild", { reason: change.type, file: change.relativePath });
|
|
348
|
+
}
|
|
349
|
+
}
|
|
341
350
|
else {
|
|
342
351
|
// Free: still rebuild graph on structural changes, but skip AI
|
|
343
352
|
if (change.type === "added" || change.type === "deleted") {
|
|
@@ -366,6 +375,18 @@ function createWebServer(getGraphFn, initialFileCache, switchProjectFn, getProje
|
|
|
366
375
|
sseClients: sseClients.size,
|
|
367
376
|
});
|
|
368
377
|
});
|
|
378
|
+
// POST /api/toggle-realtime-ai — Enable or disable real-time AI analysis
|
|
379
|
+
app.post("/api/toggle-realtime-ai", (req, res) => {
|
|
380
|
+
const { enabled } = req.body;
|
|
381
|
+
if (typeof enabled === "boolean") {
|
|
382
|
+
realtimeAIEnabled = enabled;
|
|
383
|
+
}
|
|
384
|
+
else {
|
|
385
|
+
realtimeAIEnabled = !realtimeAIEnabled;
|
|
386
|
+
}
|
|
387
|
+
console.error(`[syke:ai] Real-time AI analysis ${realtimeAIEnabled ? "ENABLED" : "DISABLED"}`);
|
|
388
|
+
res.json({ realtimeAIEnabled });
|
|
389
|
+
});
|
|
369
390
|
// GET /api/graph — Cytoscape.js compatible JSON
|
|
370
391
|
app.get("/api/graph", (_req, res) => {
|
|
371
392
|
const graph = getGraphFn();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@syke1/mcp-server",
|
|
3
|
-
"version": "1.4.
|
|
3
|
+
"version": "1.4.17",
|
|
4
4
|
"mcpName": "io.github.khalomsky/syke",
|
|
5
5
|
"description": "AI code impact analysis MCP server — dependency graphs, cascade detection, and a mandatory build gate for AI coding agents",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
},
|
|
19
19
|
"repository": {
|
|
20
20
|
"type": "git",
|
|
21
|
-
"url": "https://github.com/khalomsky/syke.git"
|
|
21
|
+
"url": "git+https://github.com/khalomsky/syke.git"
|
|
22
22
|
},
|
|
23
23
|
"keywords": [
|
|
24
24
|
"mcp",
|