@syke1/mcp-server 1.4.15 → 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.
@@ -38,7 +38,6 @@ const fs = __importStar(require("fs"));
38
38
  const path = __importStar(require("path"));
39
39
  const provider_1 = require("./provider");
40
40
  const context_extractor_1 = require("./context-extractor");
41
- const config_1 = require("../config");
42
41
  function readFileContent(filePath) {
43
42
  try {
44
43
  return fs.readFileSync(filePath, "utf-8");
@@ -49,27 +48,6 @@ function readFileContent(filePath) {
49
48
  }
50
49
  function buildSystemPrompt(languages) {
51
50
  const langNames = languages.length > 0 ? languages.join("/") : "source";
52
- const ko = (0, config_1.getLanguage)() === "ko";
53
- if (ko) {
54
- return `당신은 ${langNames} 코드 영향도 분석 전문가입니다.
55
- 주어진 파일의 소스코드와 이 파일에 의존하는 파일들의 코드를 분석하여,
56
- 이 파일을 수정할 때 어떤 부분이 깨질 수 있는지 구체적으로 설명해주세요.
57
-
58
- 분석 포맷:
59
- ## 핵심 역할
60
- 이 파일이 프로젝트에서 하는 역할을 한 문장으로 설명
61
-
62
- ## 수정 시 위험 포인트
63
- 수정 시 깨질 수 있는 구체적인 부분들 (함수명, 클래스명 포함)
64
-
65
- ## 영향받는 파일 분석
66
- 의존 파일들이 이 파일의 어떤 부분을 사용하는지 구체적으로
67
-
68
- ## 안전한 수정 가이드
69
- 이 파일을 수정할 때 주의할 점과 추천 접근법
70
-
71
- 한국어로 답변하세요. 간결하되 구체적으로 작성하세요.`;
72
- }
73
51
  return `You are an expert in ${langNames} code impact analysis.
74
52
  Analyze the source code of the given file and its dependents to identify
75
53
  what could break when this file is modified.
@@ -91,17 +69,12 @@ Be concise but specific.`;
91
69
  }
92
70
  async function analyzeWithAI(filePath, impactResult, graph) {
93
71
  const provider = (0, provider_1.getAIProvider)();
94
- const ko = (0, config_1.getLanguage)() === "ko";
95
72
  if (!provider) {
96
- return ko
97
- ? "AI 분석 비활성화 — GEMINI_KEY, OPENAI_KEY, 또는 ANTHROPIC_KEY를 설정하세요."
98
- : "AI analysis disabled — set GEMINI_KEY, OPENAI_KEY, or ANTHROPIC_KEY.";
73
+ return "AI analysis disabled — set GEMINI_KEY, OPENAI_KEY, or ANTHROPIC_KEY.";
99
74
  }
100
75
  const targetSource = readFileContent(filePath);
101
76
  if (!targetSource) {
102
- return ko
103
- ? `파일을 읽을 수 없습니다: ${filePath}`
104
- : `Cannot read file: ${filePath}`;
77
+ return `Cannot read file: ${filePath}`;
105
78
  }
106
79
  const codeBlockLang = graph.languages[0] || "text";
107
80
  // Build smart context for the target file
@@ -117,20 +90,7 @@ async function analyzeWithAI(filePath, impactResult, graph) {
117
90
  dependentSources.push(`### ${rel}\n\`\`\`${codeBlockLang}\n${smartDep}\n\`\`\``);
118
91
  }
119
92
  }
120
- const userPrompt = ko
121
- ? `## 분석 대상 파일: ${impactResult.relativePath}
122
- - 위험도: ${impactResult.riskLevel}
123
- - 직접 의존 파일 수: ${impactResult.directDependents.length}
124
- - 전이적 의존 파일 수: ${impactResult.transitiveDependents.length}
125
- - 총 영향 파일 수: ${impactResult.totalImpacted}
126
-
127
- ### 대상 파일 소스코드
128
- \`\`\`${codeBlockLang}
129
- ${smartTarget}
130
- \`\`\`
131
-
132
- ${dependentSources.length > 0 ? `### 이 파일에 의존하는 파일들 (상위 ${dependentSources.length}개)\n${dependentSources.join("\n\n")}` : "이 파일에 의존하는 내부 파일이 없습니다."}`
133
- : `## Target file: ${impactResult.relativePath}
93
+ const userPrompt = `## Target file: ${impactResult.relativePath}
134
94
  - Risk level: ${impactResult.riskLevel}
135
95
  - Direct dependents: ${impactResult.directDependents.length}
136
96
  - Transitive dependents: ${impactResult.transitiveDependents.length}
@@ -147,8 +107,6 @@ ${dependentSources.length > 0 ? `### Files depending on this file (top ${depende
147
107
  return await provider.analyze(systemPrompt, userPrompt);
148
108
  }
149
109
  catch (err) {
150
- return ko
151
- ? `AI 분석 중 오류 발생: ${err.message || err}`
152
- : `AI analysis error: ${err.message || err}`;
110
+ return `AI analysis error: ${err.message || err}`;
153
111
  }
154
112
  }
@@ -35,41 +35,43 @@ 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");
41
- const config_1 = require("../config");
42
- function getSystemPrompt() {
43
- const ko = (0, config_1.getLanguage)() === "ko";
44
- if (ko) {
45
- return `당신은 20년 경력의 풀스택 아키텍트이자, 코드 영향도 감시 AI입니다.
46
- 역할: 파일이 수정/추가/삭제될 때, 빌드 전에 잠재적 오류와 연쇄 영향을 감지합니다.
47
-
48
- 분석 원칙:
49
- 1. import/export 깨짐: 삭제/이름변경된 클래스/함수/변수가 다른 파일에서 참조되는지 확인
50
- 2. 타입 불일치: 매개변수 타입, 반환 타입 변경이 호출부와 맞는지 확인
51
- 3. 상태 관리 연쇄: Provider/Notifier 변경이 UI와 비즈니스 로직에 미치는 영향
52
- 4. 라우팅 영향: GoRouter 경로/매개변수 변경이 네비게이션에 미치는 영향
53
- 5. 누락된 초기화: 새로 추가된 Provider가 적절히 등록되었는지
54
-
55
- 응답 형식 (반드시 JSON):
56
- {
57
- "riskLevel": "CRITICAL|HIGH|MEDIUM|LOW|SAFE",
58
- "summary": "한 요약",
59
- "brokenImports": ["깨질 있는 import 목록"],
60
- "sideEffects": ["예상되는 부작용 목록"],
61
- "warnings": ["주의사항 목록"],
62
- "suggestion": "추천 조치"
63
- }
64
-
65
- CRITICAL: 빌드 실패 확실
66
- HIGH: 런타임 오류 가능
67
- MEDIUM: 기능 동작 변경 가능
68
- LOW: 사소한 영향
69
- SAFE: 안전한 변경
70
-
71
- JSON만 응답하세요. 설명 텍스트 없이 순수 JSON만.`;
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();
72
68
  }
69
+ return callTimestamps.length >= RATE_LIMIT_MAX;
70
+ }
71
+ function recordCall() {
72
+ callTimestamps.push(Date.now());
73
+ }
74
+ function getSystemPrompt() {
73
75
  return `You are a senior full-stack architect and code impact monitoring AI with 20 years of experience.
74
76
  Role: Detect potential errors and cascading impacts before build when files are modified/added/deleted.
75
77
 
@@ -106,7 +108,6 @@ async function analyzeChangeRealtime(change, graph, getFileContent) {
106
108
  const start = Date.now();
107
109
  const relPath = change.relativePath;
108
110
  const codeBlockLang = graph.languages[0] || "text";
109
- const ko = (0, config_1.getLanguage)() === "ko";
110
111
  // Get impacted files from graph
111
112
  const absPath = path.normalize(path.join(graph.sourceDir, relPath));
112
113
  let affectedNodes = [];
@@ -130,13 +131,11 @@ async function analyzeChangeRealtime(change, graph, getFileContent) {
130
131
  // Build diff summary with signature changes
131
132
  let diffSummary = "";
132
133
  if (change.type === "deleted") {
133
- const label = ko ? "파일이 삭제됨. 이전 내용:" : "File deleted. Previous content:";
134
- diffSummary = `${label}\n\`\`\`${codeBlockLang}\n${(change.oldContent || "").split("\n").slice(0, 40).join("\n")}\n\`\`\``;
134
+ diffSummary = `File deleted. Previous content:\n\`\`\`${codeBlockLang}\n${(change.oldContent || "").split("\n").slice(0, 40).join("\n")}\n\`\`\``;
135
135
  }
136
136
  else if (change.type === "added") {
137
137
  const smartNew = (0, context_extractor_1.buildSmartContext)(change.newContent || "", codeBlockLang);
138
- const label = ko ? "새 파일 추가됨:" : "New file added:";
139
- diffSummary = `${label}\n\`\`\`${codeBlockLang}\n${smartNew}\n\`\`\``;
138
+ diffSummary = `New file added:\n\`\`\`${codeBlockLang}\n${smartNew}\n\`\`\``;
140
139
  }
141
140
  else {
142
141
  // Modified — include signature diff
@@ -147,50 +146,31 @@ async function analyzeChangeRealtime(change, graph, getFileContent) {
147
146
  return `- L${d.line}: ${d.old}`;
148
147
  return `~ L${d.line}: ${d.old} → ${d.new}`;
149
148
  });
150
- const label = ko
151
- ? `변경된 라인 (${change.diff.length}개 중 상위 30개):`
152
- : `Changed lines (top 30 of ${change.diff.length}):`;
153
- diffSummary = `${label}\n\`\`\`\n${diffLines.join("\n")}\n\`\`\``;
149
+ diffSummary = `Changed lines (top 30 of ${change.diff.length}):\n\`\`\`\n${diffLines.join("\n")}\n\`\`\``;
154
150
  // Add structural signature changes
155
151
  if (change.oldContent && change.newContent) {
156
152
  const sigChanges = (0, context_extractor_1.diffSignatures)(change.oldContent, change.newContent, codeBlockLang);
157
153
  if (sigChanges.length > 0) {
158
- diffSummary += ko
159
- ? "\n\n### 구조적 변경 (시그니처 비교)"
160
- : "\n\n### Structural changes (signature diff)";
154
+ diffSummary += "\n\n### Structural changes (signature diff)";
161
155
  for (const sc of sigChanges) {
162
156
  if (sc.type === "added") {
163
- diffSummary += ko ? `\n+ 추가: ${sc.newSignature}` : `\n+ Added: ${sc.newSignature}`;
157
+ diffSummary += `\n+ Added: ${sc.newSignature}`;
164
158
  }
165
159
  else if (sc.type === "removed") {
166
- diffSummary += ko ? `\n- 삭제: ${sc.oldSignature}` : `\n- Removed: ${sc.oldSignature}`;
160
+ diffSummary += `\n- Removed: ${sc.oldSignature}`;
167
161
  }
168
162
  else {
169
- diffSummary += ko
170
- ? `\n~ 변경: ${sc.oldSignature}\n → ${sc.newSignature}`
171
- : `\n~ Changed: ${sc.oldSignature}\n → ${sc.newSignature}`;
163
+ diffSummary += `\n~ Changed: ${sc.oldSignature}\n → ${sc.newSignature}`;
172
164
  }
173
165
  }
174
166
  }
175
167
  }
176
168
  if (change.newContent) {
177
169
  const smartNew = (0, context_extractor_1.buildSmartContext)(change.newContent, codeBlockLang);
178
- const label2 = ko ? "전체 수정 후 파일:" : "Full file after modification:";
179
- diffSummary += `\n\n${label2}\n\`\`\`${codeBlockLang}\n${smartNew}\n\`\`\``;
170
+ diffSummary += `\n\nFull file after modification:\n\`\`\`${codeBlockLang}\n${smartNew}\n\`\`\``;
180
171
  }
181
172
  }
182
- const userPrompt = ko
183
- ? `## 파일 변경 감지: ${relPath}
184
- 변경 유형: ${change.type.toUpperCase()}
185
- 프로젝트 언어: ${graph.languages.join(", ") || "unknown"}
186
- 영향받는 파일 수: ${affectedNodes.length}
187
-
188
- ${diffSummary}
189
-
190
- ${connectedFiles.length > 0 ? `## 연결된 파일들 (${connectedFiles.length}개)\n${connectedFiles.join("\n\n")}` : "연결된 파일 없음"}
191
-
192
- 이 변경이 프로젝트에 미치는 영향을 분석하세요.`
193
- : `## File change detected: ${relPath}
173
+ const userPrompt = `## File change detected: ${relPath}
194
174
  Change type: ${change.type.toUpperCase()}
195
175
  Project languages: ${graph.languages.join(", ") || "unknown"}
196
176
  Affected files: ${affectedNodes.length}
@@ -200,19 +180,46 @@ ${diffSummary}
200
180
  ${connectedFiles.length > 0 ? `## Connected files (${connectedFiles.length})\n${connectedFiles.join("\n\n")}` : "No connected files"}
201
181
 
202
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
+ }
203
209
  try {
204
210
  const provider = (0, provider_1.getAIProvider)();
205
211
  if (!provider) {
206
212
  throw new Error("No AI provider available (set GEMINI_KEY, OPENAI_KEY, or ANTHROPIC_KEY)");
207
213
  }
214
+ recordCall();
208
215
  const parsed = await provider.analyzeJSON(getSystemPrompt(), userPrompt);
209
216
  const analysisMs = Date.now() - start;
210
- return {
217
+ const result = {
211
218
  file: relPath,
212
219
  changeType: change.type,
213
220
  timestamp: change.timestamp,
214
221
  riskLevel: parsed.riskLevel || "LOW",
215
- summary: parsed.summary || (ko ? "분석 완료" : "Analysis complete"),
222
+ summary: parsed.summary || "Analysis complete",
216
223
  brokenImports: parsed.brokenImports || [],
217
224
  sideEffects: parsed.sideEffects || [],
218
225
  warnings: parsed.warnings || [],
@@ -220,6 +227,11 @@ Analyze the impact of this change on the project.`;
220
227
  affectedNodes,
221
228
  analysisMs,
222
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;
223
235
  }
224
236
  catch (err) {
225
237
  const analysisMs = Date.now() - start;
@@ -229,13 +241,11 @@ Analyze the impact of this change on the project.`;
229
241
  changeType: change.type,
230
242
  timestamp: change.timestamp,
231
243
  riskLevel: affectedNodes.length >= 10 ? "HIGH" : affectedNodes.length >= 5 ? "MEDIUM" : "LOW",
232
- summary: ko
233
- ? `AI 분석 실패 — 그래프 기반 분석: ${affectedNodes.length}개 파일 영향`
234
- : `AI analysis failed — graph-based analysis: ${affectedNodes.length} files impacted`,
244
+ summary: `AI analysis failed — graph-based analysis: ${affectedNodes.length} files impacted`,
235
245
  brokenImports: [],
236
246
  sideEffects: [],
237
- warnings: [ko ? `AI 분석 오류: ${err.message}` : `AI analysis error: ${err.message}`],
238
- suggestion: ko ? "수동 확인 필요" : "Manual review required",
247
+ warnings: [`AI analysis error: ${err.message}`],
248
+ suggestion: "Manual review required",
239
249
  affectedNodes,
240
250
  analysisMs,
241
251
  };
package/dist/config.d.ts CHANGED
@@ -4,7 +4,6 @@ interface SykeConfig {
4
4
  openaiKey?: string;
5
5
  anthropicKey?: string;
6
6
  aiProvider?: string;
7
- language?: string;
8
7
  port?: number;
9
8
  }
10
9
  /**
@@ -19,10 +18,6 @@ export declare function getAllConfig(): Record<string, string | undefined>;
19
18
  * Set a config value in ~/.syke/config.json
20
19
  */
21
20
  export declare function setConfig(key: keyof SykeConfig, value: string | null): void;
22
- /**
23
- * Detect language: config > env > system locale > default "en"
24
- */
25
- export declare function getLanguage(): "ko" | "en";
26
21
  export declare const CONFIG_DIR_PATH: string;
27
22
  export declare const CONFIG_FILE_PATH: string;
28
23
  export {};
package/dist/config.js CHANGED
@@ -37,7 +37,6 @@ exports.CONFIG_FILE_PATH = exports.CONFIG_DIR_PATH = void 0;
37
37
  exports.getConfig = getConfig;
38
38
  exports.getAllConfig = getAllConfig;
39
39
  exports.setConfig = setConfig;
40
- exports.getLanguage = getLanguage;
41
40
  /**
42
41
  * Central config reader for SYKE MCP Server.
43
42
  *
@@ -118,21 +117,5 @@ function setConfig(key, value) {
118
117
  // ignore write errors
119
118
  }
120
119
  }
121
- /**
122
- * Detect language: config > env > system locale > default "en"
123
- */
124
- function getLanguage() {
125
- const configured = getConfig("language", "SYKE_LANGUAGE");
126
- if (configured) {
127
- return configured.startsWith("ko") ? "ko" : "en";
128
- }
129
- // System locale detection
130
- const locale = (process.env.LANG ||
131
- process.env.LC_ALL ||
132
- process.env.LANGUAGE ||
133
- Intl.DateTimeFormat().resolvedOptions().locale ||
134
- "").toLowerCase();
135
- return locale.startsWith("ko") ? "ko" : "en";
136
- }
137
120
  exports.CONFIG_DIR_PATH = CONFIG_DIR;
138
121
  exports.CONFIG_FILE_PATH = CONFIG_FILE;
package/dist/index.js CHANGED
@@ -124,7 +124,7 @@ async function main() {
124
124
  };
125
125
  process.on("SIGINT", shutdown);
126
126
  process.on("SIGTERM", shutdown);
127
- const server = new index_js_1.Server({ name: "syke", version: "1.4.15" }, { capabilities: { tools: {} } });
127
+ const server = new index_js_1.Server({ name: "syke", version: "1.4.16" }, { capabilities: { tools: {} } });
128
128
  // List tools
129
129
  server.setRequestHandler(types_js_1.ListToolsRequestSchema, async () => ({
130
130
  tools: [
@@ -204,7 +204,7 @@ async function main() {
204
204
  },
205
205
  {
206
206
  name: "ai_analyze",
207
- description: "Use AI (Gemini/OpenAI/Claude) to perform semantic analysis on a file. Reads the file's source code and its dependents to explain what might break when modified and how to safely make changes. Returns Korean analysis.",
207
+ description: "Use AI (Gemini/OpenAI/Claude) to perform semantic analysis on a file. Reads the file's source code and its dependents to explain what might break when modified and how to safely make changes.",
208
208
  inputSchema: {
209
209
  type: "object",
210
210
  properties: {
@@ -488,7 +488,7 @@ async function main() {
488
488
  }
489
489
  });
490
490
  // Pre-warm the graph (skip if no project root — e.g. Smithery scan)
491
- console.error(`[syke] Starting SYKE MCP Server v1.4.15`);
491
+ console.error(`[syke] Starting SYKE MCP Server v1.4.16`);
492
492
  console.error(`[syke] License: ${licenseStatus.plan.toUpperCase()} (${licenseStatus.source})`);
493
493
  if (licenseStatus.expiresAt) {
494
494
  console.error(`[syke] Expires: ${licenseStatus.expiresAt}`);
@@ -660,7 +660,7 @@ main().catch((err) => {
660
660
  * See: https://smithery.ai/docs/deploy#sandbox-server
661
661
  */
662
662
  function createSandboxServer() {
663
- const sandboxServer = new index_js_1.Server({ name: "syke", version: "1.4.15" }, { capabilities: { tools: {} } });
663
+ const sandboxServer = new index_js_1.Server({ name: "syke", version: "1.4.16" }, { capabilities: { tools: {} } });
664
664
  sandboxServer.setRequestHandler(types_js_1.ListToolsRequestSchema, async () => ({
665
665
  tools: [
666
666
  {
@@ -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.15",
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",