@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.
@@ -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
- return {
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 = 300;
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();
@@ -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.16",
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",