@syke1/mcp-server 1.2.0 → 1.3.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 CHANGED
@@ -23,7 +23,10 @@ Works with **Claude Code**, **Cursor**, **Windsurf**, and any MCP-compatible AI
23
23
  "command": "npx",
24
24
  "args": ["@syke1/mcp-server@latest"],
25
25
  "env": {
26
- "SYKE_LICENSE_KEY": "your-key-here"
26
+ "SYKE_LICENSE_KEY": "your-key-here",
27
+ "GEMINI_KEY": "your-gemini-key",
28
+ "OPENAI_KEY": "your-openai-key",
29
+ "ANTHROPIC_KEY": "your-anthropic-key"
27
30
  }
28
31
  }
29
32
  }
@@ -39,7 +42,8 @@ Works with **Claude Code**, **Cursor**, **Windsurf**, and any MCP-compatible AI
39
42
  "command": "npx",
40
43
  "args": ["@syke1/mcp-server@latest"],
41
44
  "env": {
42
- "SYKE_LICENSE_KEY": "your-key-here"
45
+ "SYKE_LICENSE_KEY": "your-key-here",
46
+ "GEMINI_KEY": "your-gemini-key"
43
47
  }
44
48
  }
45
49
  }
@@ -55,13 +59,16 @@ Works with **Claude Code**, **Cursor**, **Windsurf**, and any MCP-compatible AI
55
59
  "command": "npx",
56
60
  "args": ["@syke1/mcp-server@latest"],
57
61
  "env": {
58
- "SYKE_LICENSE_KEY": "your-key-here"
62
+ "SYKE_LICENSE_KEY": "your-key-here",
63
+ "GEMINI_KEY": "your-gemini-key"
59
64
  }
60
65
  }
61
66
  }
62
67
  }
63
68
  ```
64
69
 
70
+ > **Note:** You only need ONE AI key. SYKE auto-selects: Gemini > OpenAI > Anthropic. Set `SYKE_AI_PROVIDER` to force a specific one.
71
+
65
72
  > **Windows note:** If `npx` is not found, use the full path: `"command": "C:\\Program Files\\nodejs\\npx.cmd"`
66
73
 
67
74
  ### 2. Restart your AI agent
@@ -84,9 +91,22 @@ A web dashboard opens automatically at `http://localhost:3333` showing your live
84
91
  | `get_dependencies` | Lists internal imports (forward dependencies) of a file. |
85
92
  | `get_hub_files` | Ranks files by how many other files depend on them. |
86
93
  | `refresh_graph` | Re-scans all source files and rebuilds the dependency graph. |
87
- | `ai_analyze` | **Pro** — Gemini AI semantic analysis of a file and its dependents. |
94
+ | `ai_analyze` | **Pro** — AI semantic analysis (Gemini/OpenAI/Claude) of a file and its dependents. |
88
95
  | `check_warnings` | Real-time monitoring alerts for file changes that may break dependents. |
89
96
 
97
+ ### Multi-AI Provider Support
98
+
99
+ SYKE supports three AI providers for semantic analysis. Bring your own key:
100
+
101
+ | Provider | Model | Env Variable |
102
+ |----------|-------|-------------|
103
+ | Google Gemini | `gemini-2.5-flash` | `GEMINI_KEY` |
104
+ | OpenAI | `gpt-4o-mini` | `OPENAI_KEY` |
105
+ | Anthropic | `claude-sonnet-4-20250514` | `ANTHROPIC_KEY` |
106
+
107
+ **Auto-selection:** SYKE uses the first available key (Gemini > OpenAI > Anthropic).
108
+ **Force provider:** Set `SYKE_AI_PROVIDER=openai` (or `gemini`, `anthropic`) to override.
109
+
90
110
  ### Language Support
91
111
 
92
112
  Auto-detected, zero-config: **Dart/Flutter**, **TypeScript/JavaScript**, **Python**, **Go**, **Rust**, **Java**, **C++**, **Ruby**.
@@ -110,7 +130,7 @@ Live dependency graph visualization at `localhost:3333` with:
110
130
  - All tools including get_hub_files, ai_analyze, check_warnings
111
131
  - Real-time cascade monitoring (SSE)
112
132
  - Cycle detection & simulation
113
- - AI semantic analysis (BYOK Gemini key)
133
+ - AI semantic analysis (BYOK Gemini, OpenAI, or Claude)
114
134
  - Priority support
115
135
 
116
136
  Get your license key at [syke.cloud/dashboard](https://syke.cloud/dashboard/).
@@ -120,7 +140,10 @@ Get your license key at [syke.cloud/dashboard](https://syke.cloud/dashboard/).
120
140
  | Environment Variable | Description | Required |
121
141
  |---------------------|-------------|----------|
122
142
  | `SYKE_LICENSE_KEY` | Pro license key from dashboard | No (Free tier works without) |
123
- | `GEMINI_KEY` | Google Gemini API key for `ai_analyze` | Pro only |
143
+ | `GEMINI_KEY` | Google Gemini API key for `ai_analyze` | No (any one AI key) |
144
+ | `OPENAI_KEY` | OpenAI API key for `ai_analyze` | No (any one AI key) |
145
+ | `ANTHROPIC_KEY` | Anthropic API key for `ai_analyze` | No (any one AI key) |
146
+ | `SYKE_AI_PROVIDER` | Force AI provider: `gemini`, `openai`, or `anthropic` | No (auto-selects) |
124
147
  | `SYKE_WEB_PORT` | Dashboard port (default: 3333) | No |
125
148
  | `SYKE_NO_BROWSER` | Set to `1` to disable auto-open browser | No |
126
149
  | `SYKE_currentProjectRoot` | Override auto-detected project root | No |
@@ -36,18 +36,8 @@ Object.defineProperty(exports, "__esModule", { value: true });
36
36
  exports.analyzeWithAI = analyzeWithAI;
37
37
  const fs = __importStar(require("fs"));
38
38
  const path = __importStar(require("path"));
39
- const generative_ai_1 = require("@google/generative-ai");
40
- let genAI = null;
41
- function getGenAI() {
42
- if (!genAI) {
43
- const key = process.env.GEMINI_KEY;
44
- if (!key) {
45
- throw new Error("GEMINI_KEY environment variable is required for AI analysis");
46
- }
47
- genAI = new generative_ai_1.GoogleGenerativeAI(key);
48
- }
49
- return genAI;
50
- }
39
+ const provider_1 = require("./provider");
40
+ const context_extractor_1 = require("./context-extractor");
51
41
  function readFileContent(filePath) {
52
42
  try {
53
43
  return fs.readFileSync(filePath, "utf-8");
@@ -78,20 +68,26 @@ function buildSystemPrompt(languages) {
78
68
  한국어로 답변하세요. 간결하되 구체적으로 작성하세요.`;
79
69
  }
80
70
  async function analyzeWithAI(filePath, impactResult, graph) {
81
- const ai = getGenAI();
82
- const model = ai.getGenerativeModel({ model: "gemini-2.5-flash" });
71
+ const provider = (0, provider_1.getAIProvider)();
72
+ if (!provider) {
73
+ return "AI 분석 비활성화 — GEMINI_KEY, OPENAI_KEY, 또는 ANTHROPIC_KEY를 설정하세요.";
74
+ }
83
75
  const targetSource = readFileContent(filePath);
84
76
  if (!targetSource) {
85
77
  return `파일을 읽을 수 없습니다: ${filePath}`;
86
78
  }
87
79
  const codeBlockLang = graph.languages[0] || "text";
80
+ // Build smart context for the target file
81
+ const smartTarget = (0, context_extractor_1.buildSmartContext)(targetSource, codeBlockLang);
82
+ // Build smart context for dependent files (top 5)
88
83
  const directDeps = (graph.reverse.get(path.normalize(filePath)) || []).slice(0, 5);
89
84
  const dependentSources = [];
90
85
  for (const dep of directDeps) {
91
86
  const source = readFileContent(dep);
92
87
  if (source) {
93
88
  const rel = path.relative(graph.sourceDir, dep).replace(/\\/g, "/");
94
- dependentSources.push(`### ${rel}\n\`\`\`${codeBlockLang}\n${source}\n\`\`\``);
89
+ const smartDep = (0, context_extractor_1.buildSmartContext)(source, codeBlockLang);
90
+ dependentSources.push(`### ${rel}\n\`\`\`${codeBlockLang}\n${smartDep}\n\`\`\``);
95
91
  }
96
92
  }
97
93
  const userPrompt = `## 분석 대상 파일: ${impactResult.relativePath}
@@ -102,17 +98,13 @@ async function analyzeWithAI(filePath, impactResult, graph) {
102
98
 
103
99
  ### 대상 파일 소스코드
104
100
  \`\`\`${codeBlockLang}
105
- ${targetSource}
101
+ ${smartTarget}
106
102
  \`\`\`
107
103
 
108
104
  ${dependentSources.length > 0 ? `### 이 파일에 의존하는 파일들 (상위 ${dependentSources.length}개)\n${dependentSources.join("\n\n")}` : "이 파일에 의존하는 내부 파일이 없습니다."}`;
109
105
  try {
110
106
  const systemPrompt = buildSystemPrompt(graph.languages);
111
- const result = await model.generateContent({
112
- contents: [{ role: "user", parts: [{ text: userPrompt }] }],
113
- systemInstruction: { role: "model", parts: [{ text: systemPrompt }] },
114
- });
115
- return result.response.text();
107
+ return await provider.analyze(systemPrompt, userPrompt);
116
108
  }
117
109
  catch (err) {
118
110
  return `AI 분석 중 오류 발생: ${err.message || err}`;
@@ -0,0 +1,32 @@
1
+ /**
2
+ * Semantic context extractor — structural code analysis for AI prompts.
3
+ * Extracts function/class/type signatures, compares changes, builds smart context.
4
+ */
5
+ export interface CodeSignature {
6
+ type: "function" | "class" | "interface" | "type" | "variable";
7
+ name: string;
8
+ signature: string;
9
+ exported: boolean;
10
+ line: number;
11
+ }
12
+ export interface SignatureChange {
13
+ type: "added" | "removed" | "modified";
14
+ name: string;
15
+ oldSignature?: string;
16
+ newSignature?: string;
17
+ }
18
+ /**
19
+ * Extract function/class/type signatures from source code.
20
+ */
21
+ export declare function extractSignatures(content: string, lang: string): CodeSignature[];
22
+ /**
23
+ * Compare signatures between old and new file content.
24
+ * Returns structural changes (added/removed/modified declarations).
25
+ */
26
+ export declare function diffSignatures(oldContent: string, newContent: string, lang: string): SignatureChange[];
27
+ /**
28
+ * Build a smart context summary of a file for AI prompts.
29
+ * Short files (<100 lines) are passed through as-is.
30
+ * Longer files get: imports + exported signatures + body summary.
31
+ */
32
+ export declare function buildSmartContext(content: string, lang: string, maxLines?: number): string;
@@ -0,0 +1,224 @@
1
+ "use strict";
2
+ /**
3
+ * Semantic context extractor — structural code analysis for AI prompts.
4
+ * Extracts function/class/type signatures, compares changes, builds smart context.
5
+ */
6
+ Object.defineProperty(exports, "__esModule", { value: true });
7
+ exports.extractSignatures = extractSignatures;
8
+ exports.diffSignatures = diffSignatures;
9
+ exports.buildSmartContext = buildSmartContext;
10
+ const LANG_PATTERNS = {
11
+ typescript: {
12
+ patterns: [
13
+ { type: "function", regex: /^(export\s+)?(async\s+)?function\s+(\w+)\s*(<[^>]*>)?\s*\([^)]*\)/gm },
14
+ { type: "class", regex: /^(export\s+)?(abstract\s+)?class\s+(\w+)(\s+extends\s+\w+)?(\s+implements\s+[\w,\s]+)?/gm },
15
+ { type: "interface", regex: /^(export\s+)?interface\s+(\w+)(\s+extends\s+[\w,\s]+)?/gm },
16
+ { type: "type", regex: /^(export\s+)?type\s+(\w+)\s*(<[^>]*>)?\s*=/gm },
17
+ { type: "variable", regex: /^(export\s+)?(const|let)\s+(\w+)\s*[=:]/gm },
18
+ ],
19
+ },
20
+ dart: {
21
+ patterns: [
22
+ { type: "class", regex: /^(abstract\s+)?class\s+(\w+)(\s+extends\s+\w+)?(\s+with\s+[\w,\s]+)?(\s+implements\s+[\w,\s]+)?/gm },
23
+ { type: "function", regex: /^\s*(static\s+)?(Future<[^>]+>|void|String|int|double|bool|List<[^>]+>|Map<[^>]+>|Set<[^>]+>|\w+\??)\s+(\w+)\s*\([^)]*\)/gm },
24
+ { type: "variable", regex: /^\s*(static\s+)?(final|const|late\s+final)\s+(\w+\??)\s+(\w+)/gm },
25
+ ],
26
+ },
27
+ python: {
28
+ patterns: [
29
+ { type: "function", regex: /^(async\s+)?def\s+(\w+)\s*\([^)]*\)(\s*->\s*\w+)?/gm },
30
+ { type: "class", regex: /^class\s+(\w+)(\([^)]*\))?/gm },
31
+ { type: "variable", regex: /^(\w+)\s*:\s*\w+\s*=/gm },
32
+ ],
33
+ },
34
+ go: {
35
+ patterns: [
36
+ { type: "function", regex: /^func\s+(\([^)]+\)\s+)?(\w+)\s*\([^)]*\)(\s*\([^)]*\)|\s*\w+)?/gm },
37
+ { type: "interface", regex: /^type\s+(\w+)\s+interface/gm },
38
+ { type: "type", regex: /^type\s+(\w+)\s+struct/gm },
39
+ ],
40
+ },
41
+ rust: {
42
+ patterns: [
43
+ { type: "function", regex: /^(pub\s+)?(async\s+)?fn\s+(\w+)\s*(<[^>]*>)?\s*\([^)]*\)(\s*->\s*[\w<>&]+)?/gm },
44
+ { type: "class", regex: /^(pub\s+)?struct\s+(\w+)/gm },
45
+ { type: "interface", regex: /^(pub\s+)?trait\s+(\w+)/gm },
46
+ { type: "type", regex: /^(pub\s+)?enum\s+(\w+)/gm },
47
+ ],
48
+ },
49
+ java: {
50
+ patterns: [
51
+ { type: "class", regex: /^(public\s+)?(abstract\s+)?class\s+(\w+)(\s+extends\s+\w+)?(\s+implements\s+[\w,\s]+)?/gm },
52
+ { type: "interface", regex: /^(public\s+)?interface\s+(\w+)(\s+extends\s+[\w,\s]+)?/gm },
53
+ { type: "function", regex: /^\s*(public|protected|private)?\s*(static\s+)?([\w<>\[\]]+)\s+(\w+)\s*\([^)]*\)/gm },
54
+ ],
55
+ },
56
+ cpp: {
57
+ patterns: [
58
+ { type: "class", regex: /^(class|struct)\s+(\w+)/gm },
59
+ { type: "function", regex: /^(virtual\s+)?(static\s+)?([\w:*&<>]+)\s+(\w+)\s*\([^)]*\)/gm },
60
+ ],
61
+ },
62
+ ruby: {
63
+ patterns: [
64
+ { type: "class", regex: /^class\s+(\w+)(\s*<\s*\w+)?/gm },
65
+ { type: "function", regex: /^\s*def\s+(self\.)?(\w+[?!]?)\s*(\([^)]*\))?/gm },
66
+ ],
67
+ },
68
+ };
69
+ // Map file extensions / language names to pattern keys
70
+ function getLangKey(lang) {
71
+ const map = {
72
+ ts: "typescript", tsx: "typescript", typescript: "typescript", javascript: "typescript", js: "typescript",
73
+ dart: "dart", flutter: "dart",
74
+ py: "python", python: "python",
75
+ go: "go",
76
+ rs: "rust", rust: "rust",
77
+ java: "java", kotlin: "java",
78
+ cpp: "cpp", "c++": "cpp", c: "cpp", h: "cpp", hpp: "cpp",
79
+ rb: "ruby", ruby: "ruby",
80
+ };
81
+ return map[lang.toLowerCase()] || "typescript"; // fallback to TS patterns
82
+ }
83
+ // ── Signature Extraction ────────────────────────────────────────────
84
+ /**
85
+ * Extract function/class/type signatures from source code.
86
+ */
87
+ function extractSignatures(content, lang) {
88
+ const langKey = getLangKey(lang);
89
+ const langPatterns = LANG_PATTERNS[langKey];
90
+ if (!langPatterns)
91
+ return [];
92
+ const lines = content.split("\n");
93
+ const signatures = [];
94
+ const seen = new Set();
95
+ for (const { type, regex } of langPatterns.patterns) {
96
+ // Reset regex state
97
+ regex.lastIndex = 0;
98
+ let match;
99
+ while ((match = regex.exec(content)) !== null) {
100
+ const matchLine = content.substring(0, match.index).split("\n").length;
101
+ const fullLine = lines[matchLine - 1]?.trim() || match[0].trim();
102
+ const exported = /^export\s/.test(fullLine) || /^pub\s/.test(fullLine);
103
+ // Extract name (last capturing group that looks like a name)
104
+ let name = "";
105
+ for (let i = match.length - 1; i >= 1; i--) {
106
+ if (match[i] && /^\w+$/.test(match[i])) {
107
+ name = match[i];
108
+ break;
109
+ }
110
+ }
111
+ if (!name)
112
+ name = fullLine.split(/[\s(<{=]/)[1] || "unknown";
113
+ const key = `${type}:${name}`;
114
+ if (seen.has(key))
115
+ continue;
116
+ seen.add(key);
117
+ signatures.push({
118
+ type,
119
+ name,
120
+ signature: fullLine,
121
+ exported,
122
+ line: matchLine,
123
+ });
124
+ }
125
+ }
126
+ return signatures.sort((a, b) => a.line - b.line);
127
+ }
128
+ // ── Signature Diff ──────────────────────────────────────────────────
129
+ /**
130
+ * Compare signatures between old and new file content.
131
+ * Returns structural changes (added/removed/modified declarations).
132
+ */
133
+ function diffSignatures(oldContent, newContent, lang) {
134
+ const oldSigs = extractSignatures(oldContent, lang);
135
+ const newSigs = extractSignatures(newContent, lang);
136
+ const oldMap = new Map(oldSigs.map((s) => [`${s.type}:${s.name}`, s]));
137
+ const newMap = new Map(newSigs.map((s) => [`${s.type}:${s.name}`, s]));
138
+ const changes = [];
139
+ // Removed
140
+ for (const [key, sig] of oldMap) {
141
+ if (!newMap.has(key)) {
142
+ changes.push({ type: "removed", name: sig.name, oldSignature: sig.signature });
143
+ }
144
+ }
145
+ // Added
146
+ for (const [key, sig] of newMap) {
147
+ if (!oldMap.has(key)) {
148
+ changes.push({ type: "added", name: sig.name, newSignature: sig.signature });
149
+ }
150
+ }
151
+ // Modified
152
+ for (const [key, newSig] of newMap) {
153
+ const oldSig = oldMap.get(key);
154
+ if (oldSig && oldSig.signature !== newSig.signature) {
155
+ changes.push({
156
+ type: "modified",
157
+ name: newSig.name,
158
+ oldSignature: oldSig.signature,
159
+ newSignature: newSig.signature,
160
+ });
161
+ }
162
+ }
163
+ return changes;
164
+ }
165
+ // ── Smart Context Builder ───────────────────────────────────────────
166
+ /**
167
+ * Build a smart context summary of a file for AI prompts.
168
+ * Short files (<100 lines) are passed through as-is.
169
+ * Longer files get: imports + exported signatures + body summary.
170
+ */
171
+ function buildSmartContext(content, lang, maxLines = 100) {
172
+ const lines = content.split("\n");
173
+ // Short files: return as-is
174
+ if (lines.length <= maxLines)
175
+ return content;
176
+ const parts = [];
177
+ const importLines = [];
178
+ let lastImportLine = 0;
179
+ // Collect imports
180
+ for (let i = 0; i < lines.length; i++) {
181
+ const line = lines[i].trim();
182
+ if (line.startsWith("import ") ||
183
+ line.startsWith("from ") ||
184
+ line.startsWith("require(") ||
185
+ line.startsWith("const ") && line.includes("require(") ||
186
+ line.startsWith("use ") ||
187
+ line.startsWith("#include") ||
188
+ line.startsWith("package ")) {
189
+ importLines.push(lines[i]);
190
+ lastImportLine = i;
191
+ }
192
+ // Stop scanning after a non-import, non-blank line beyond the header
193
+ if (i > 20 && line && !line.startsWith("import") && !line.startsWith("from") && !line.startsWith("//") && !line.startsWith("#") && !line.startsWith("*") && !line.startsWith("/*")) {
194
+ break;
195
+ }
196
+ }
197
+ if (importLines.length > 0) {
198
+ parts.push("// ── Imports ──");
199
+ parts.push(...importLines);
200
+ parts.push("");
201
+ }
202
+ // Collect exported signatures
203
+ const sigs = extractSignatures(content, lang).filter((s) => s.exported);
204
+ if (sigs.length > 0) {
205
+ parts.push("// ── Exported Declarations ──");
206
+ for (const sig of sigs) {
207
+ parts.push(`${sig.signature} // L${sig.line}`);
208
+ }
209
+ parts.push("");
210
+ }
211
+ // All signatures (non-exported)
212
+ const privateSigs = extractSignatures(content, lang).filter((s) => !s.exported);
213
+ if (privateSigs.length > 0) {
214
+ parts.push("// ── Internal Declarations ──");
215
+ for (const sig of privateSigs) {
216
+ parts.push(`${sig.signature} // L${sig.line}`);
217
+ }
218
+ parts.push("");
219
+ }
220
+ const bodyLines = lines.length - lastImportLine - 1;
221
+ parts.push(`// ... (${bodyLines} lines of implementation omitted)`);
222
+ parts.push(`// Total: ${lines.length} lines`);
223
+ return parts.join("\n");
224
+ }
@@ -0,0 +1,21 @@
1
+ /**
2
+ * AI Provider abstraction — Multi-AI support (Gemini, OpenAI, Anthropic)
3
+ * Automatically selects the best available provider based on API keys.
4
+ */
5
+ export interface AIProvider {
6
+ name: string;
7
+ analyze(systemPrompt: string, userPrompt: string): Promise<string>;
8
+ analyzeJSON<T>(systemPrompt: string, userPrompt: string): Promise<T>;
9
+ }
10
+ /**
11
+ * Returns the best available AI provider, or null if no API key is set.
12
+ *
13
+ * Priority:
14
+ * 1. SYKE_AI_PROVIDER env var forces a specific provider
15
+ * 2. Auto-select: GEMINI_KEY > OPENAI_KEY > ANTHROPIC_KEY
16
+ */
17
+ export declare function getAIProvider(): AIProvider | null;
18
+ /**
19
+ * Human-readable name for the active AI provider (for logs/UI).
20
+ */
21
+ export declare function getProviderName(): string;
@@ -0,0 +1,174 @@
1
+ "use strict";
2
+ /**
3
+ * AI Provider abstraction — Multi-AI support (Gemini, OpenAI, Anthropic)
4
+ * Automatically selects the best available provider based on API keys.
5
+ */
6
+ Object.defineProperty(exports, "__esModule", { value: true });
7
+ exports.getAIProvider = getAIProvider;
8
+ exports.getProviderName = getProviderName;
9
+ const generative_ai_1 = require("@google/generative-ai");
10
+ // ── Gemini ──────────────────────────────────────────────────────────
11
+ class GeminiProvider {
12
+ constructor(apiKey) {
13
+ this.name = "Gemini (gemini-2.5-flash)";
14
+ this.ai = new generative_ai_1.GoogleGenerativeAI(apiKey);
15
+ }
16
+ async analyze(systemPrompt, userPrompt) {
17
+ const model = this.ai.getGenerativeModel({ model: "gemini-2.5-flash" });
18
+ const result = await model.generateContent({
19
+ contents: [{ role: "user", parts: [{ text: userPrompt }] }],
20
+ systemInstruction: { role: "model", parts: [{ text: systemPrompt }] },
21
+ });
22
+ return result.response.text();
23
+ }
24
+ async analyzeJSON(systemPrompt, userPrompt) {
25
+ const text = await this.analyze(systemPrompt, userPrompt);
26
+ let jsonStr = text;
27
+ const jsonMatch = text.match(/```(?:json)?\s*([\s\S]*?)```/);
28
+ if (jsonMatch)
29
+ jsonStr = jsonMatch[1];
30
+ return JSON.parse(jsonStr.trim());
31
+ }
32
+ }
33
+ // ── OpenAI ──────────────────────────────────────────────────────────
34
+ class OpenAIProvider {
35
+ constructor(apiKey) {
36
+ this.name = "OpenAI (gpt-4o-mini)";
37
+ this.apiKey = apiKey;
38
+ }
39
+ async analyze(systemPrompt, userPrompt) {
40
+ const res = await fetch("https://api.openai.com/v1/chat/completions", {
41
+ method: "POST",
42
+ headers: {
43
+ "Content-Type": "application/json",
44
+ Authorization: `Bearer ${this.apiKey}`,
45
+ },
46
+ body: JSON.stringify({
47
+ model: "gpt-4o-mini",
48
+ messages: [
49
+ { role: "system", content: systemPrompt },
50
+ { role: "user", content: userPrompt },
51
+ ],
52
+ temperature: 0.3,
53
+ }),
54
+ });
55
+ if (!res.ok) {
56
+ const body = await res.text();
57
+ throw new Error(`OpenAI API error ${res.status}: ${body}`);
58
+ }
59
+ const data = (await res.json());
60
+ return data.choices[0].message.content;
61
+ }
62
+ async analyzeJSON(systemPrompt, userPrompt) {
63
+ const res = await fetch("https://api.openai.com/v1/chat/completions", {
64
+ method: "POST",
65
+ headers: {
66
+ "Content-Type": "application/json",
67
+ Authorization: `Bearer ${this.apiKey}`,
68
+ },
69
+ body: JSON.stringify({
70
+ model: "gpt-4o-mini",
71
+ messages: [
72
+ { role: "system", content: systemPrompt },
73
+ { role: "user", content: userPrompt },
74
+ ],
75
+ temperature: 0.3,
76
+ response_format: { type: "json_object" },
77
+ }),
78
+ });
79
+ if (!res.ok) {
80
+ const body = await res.text();
81
+ throw new Error(`OpenAI API error ${res.status}: ${body}`);
82
+ }
83
+ const data = (await res.json());
84
+ return JSON.parse(data.choices[0].message.content);
85
+ }
86
+ }
87
+ // ── Anthropic ───────────────────────────────────────────────────────
88
+ class AnthropicProvider {
89
+ constructor(apiKey) {
90
+ this.name = "Claude (claude-sonnet-4-20250514)";
91
+ this.apiKey = apiKey;
92
+ }
93
+ async analyze(systemPrompt, userPrompt) {
94
+ const res = await fetch("https://api.anthropic.com/v1/messages", {
95
+ method: "POST",
96
+ headers: {
97
+ "Content-Type": "application/json",
98
+ "x-api-key": this.apiKey,
99
+ "anthropic-version": "2023-06-01",
100
+ },
101
+ body: JSON.stringify({
102
+ model: "claude-sonnet-4-20250514",
103
+ max_tokens: 4096,
104
+ system: systemPrompt,
105
+ messages: [{ role: "user", content: userPrompt }],
106
+ }),
107
+ });
108
+ if (!res.ok) {
109
+ const body = await res.text();
110
+ throw new Error(`Anthropic API error ${res.status}: ${body}`);
111
+ }
112
+ const data = (await res.json());
113
+ return data.content[0].text;
114
+ }
115
+ async analyzeJSON(systemPrompt, userPrompt) {
116
+ const text = await this.analyze(systemPrompt + "\n\nJSON만 응답하세요. 설명 텍스트 없이 순수 JSON만.", userPrompt);
117
+ let jsonStr = text;
118
+ const jsonMatch = text.match(/```(?:json)?\s*([\s\S]*?)```/);
119
+ if (jsonMatch)
120
+ jsonStr = jsonMatch[1];
121
+ return JSON.parse(jsonStr.trim());
122
+ }
123
+ }
124
+ // ── Factory ─────────────────────────────────────────────────────────
125
+ let cachedProvider = undefined;
126
+ /**
127
+ * Returns the best available AI provider, or null if no API key is set.
128
+ *
129
+ * Priority:
130
+ * 1. SYKE_AI_PROVIDER env var forces a specific provider
131
+ * 2. Auto-select: GEMINI_KEY > OPENAI_KEY > ANTHROPIC_KEY
132
+ */
133
+ function getAIProvider() {
134
+ if (cachedProvider !== undefined)
135
+ return cachedProvider;
136
+ const forced = process.env.SYKE_AI_PROVIDER?.toLowerCase();
137
+ if (forced) {
138
+ if (forced === "gemini" && process.env.GEMINI_KEY) {
139
+ cachedProvider = new GeminiProvider(process.env.GEMINI_KEY);
140
+ }
141
+ else if (forced === "openai" && process.env.OPENAI_KEY) {
142
+ cachedProvider = new OpenAIProvider(process.env.OPENAI_KEY);
143
+ }
144
+ else if (forced === "anthropic" && process.env.ANTHROPIC_KEY) {
145
+ cachedProvider = new AnthropicProvider(process.env.ANTHROPIC_KEY);
146
+ }
147
+ else {
148
+ console.error(`[syke] SYKE_AI_PROVIDER=${forced} but no matching API key found`);
149
+ cachedProvider = null;
150
+ }
151
+ return cachedProvider;
152
+ }
153
+ // Auto-select
154
+ if (process.env.GEMINI_KEY) {
155
+ cachedProvider = new GeminiProvider(process.env.GEMINI_KEY);
156
+ }
157
+ else if (process.env.OPENAI_KEY) {
158
+ cachedProvider = new OpenAIProvider(process.env.OPENAI_KEY);
159
+ }
160
+ else if (process.env.ANTHROPIC_KEY) {
161
+ cachedProvider = new AnthropicProvider(process.env.ANTHROPIC_KEY);
162
+ }
163
+ else {
164
+ cachedProvider = null;
165
+ }
166
+ return cachedProvider;
167
+ }
168
+ /**
169
+ * Human-readable name for the active AI provider (for logs/UI).
170
+ */
171
+ function getProviderName() {
172
+ const provider = getAIProvider();
173
+ return provider ? provider.name : "disabled";
174
+ }
@@ -14,7 +14,7 @@ export interface RealtimeAnalysis {
14
14
  analysisMs: number;
15
15
  }
16
16
  /**
17
- * Analyze a file change in real-time using Gemini.
17
+ * Analyze a file change in real-time using the configured AI provider.
18
18
  * Receives the diff + connected files context from memory cache.
19
19
  */
20
20
  export declare function analyzeChangeRealtime(change: FileChange, graph: DependencyGraph, getFileContent: (relPath: string) => string | null): Promise<RealtimeAnalysis>;
@@ -34,9 +34,10 @@ var __importStar = (this && this.__importStar) || (function () {
34
34
  })();
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
36
  exports.analyzeChangeRealtime = analyzeChangeRealtime;
37
- const generative_ai_1 = require("@google/generative-ai");
38
37
  const analyze_impact_1 = require("../tools/analyze-impact");
39
38
  const path = __importStar(require("path"));
39
+ const provider_1 = require("./provider");
40
+ const context_extractor_1 = require("./context-extractor");
40
41
  const REALTIME_SYSTEM_PROMPT = `당신은 20년 경력의 풀스택 아키텍트이자, 코드 영향도 감시 AI입니다.
41
42
  역할: 파일이 수정/추가/삭제될 때, 빌드 전에 잠재적 오류와 연쇄 영향을 감지합니다.
42
43
 
@@ -64,18 +65,8 @@ LOW: 사소한 영향
64
65
  SAFE: 안전한 변경
65
66
 
66
67
  JSON만 응답하세요. 설명 텍스트 없이 순수 JSON만.`;
67
- let genAI = null;
68
- function getGenAI() {
69
- if (!genAI) {
70
- const key = process.env.GEMINI_KEY;
71
- if (!key)
72
- throw new Error("GEMINI_KEY required");
73
- genAI = new generative_ai_1.GoogleGenerativeAI(key);
74
- }
75
- return genAI;
76
- }
77
68
  /**
78
- * Analyze a file change in real-time using Gemini.
69
+ * Analyze a file change in real-time using the configured AI provider.
79
70
  * Receives the diff + connected files context from memory cache.
80
71
  */
81
72
  async function analyzeChangeRealtime(change, graph, getFileContent) {
@@ -89,7 +80,7 @@ async function analyzeChangeRealtime(change, graph, getFileContent) {
89
80
  const impact = (0, analyze_impact_1.analyzeImpact)(absPath, graph);
90
81
  affectedNodes = [...impact.directDependents, ...impact.transitiveDependents];
91
82
  }
92
- // Build context: changed file + top 5 connected files' content
83
+ // Build context: changed file + top 5 connected files' smart context
93
84
  const connectedFiles = [];
94
85
  const revDeps = graph.reverse.get(absPath) || [];
95
86
  const fwdDeps = graph.forward.get(absPath) || [];
@@ -98,19 +89,21 @@ async function analyzeChangeRealtime(change, graph, getFileContent) {
98
89
  const depRel = path.relative(graph.sourceDir, dep).replace(/\\/g, "/");
99
90
  const content = getFileContent(depRel);
100
91
  if (content) {
101
- const truncated = content.split("\n").slice(0, 60).join("\n");
102
- connectedFiles.push(`### ${depRel}\n\`\`\`${codeBlockLang}\n${truncated}\n\`\`\``);
92
+ const smartCtx = (0, context_extractor_1.buildSmartContext)(content, codeBlockLang);
93
+ connectedFiles.push(`### ${depRel}\n\`\`\`${codeBlockLang}\n${smartCtx}\n\`\`\``);
103
94
  }
104
95
  }
105
- // Build diff summary
96
+ // Build diff summary with signature changes
106
97
  let diffSummary = "";
107
98
  if (change.type === "deleted") {
108
99
  diffSummary = `파일이 삭제됨. 이전 내용:\n\`\`\`${codeBlockLang}\n${(change.oldContent || "").split("\n").slice(0, 40).join("\n")}\n\`\`\``;
109
100
  }
110
101
  else if (change.type === "added") {
111
- diffSummary = `새 파일 추가됨:\n\`\`\`${codeBlockLang}\n${(change.newContent || "").split("\n").slice(0, 60).join("\n")}\n\`\`\``;
102
+ const smartNew = (0, context_extractor_1.buildSmartContext)(change.newContent || "", codeBlockLang);
103
+ diffSummary = `새 파일 추가됨:\n\`\`\`${codeBlockLang}\n${smartNew}\n\`\`\``;
112
104
  }
113
105
  else {
106
+ // Modified — include signature diff
114
107
  const diffLines = change.diff.slice(0, 30).map(d => {
115
108
  if (d.type === "added")
116
109
  return `+ L${d.line}: ${d.new}`;
@@ -119,8 +112,27 @@ async function analyzeChangeRealtime(change, graph, getFileContent) {
119
112
  return `~ L${d.line}: ${d.old} → ${d.new}`;
120
113
  });
121
114
  diffSummary = `변경된 라인 (${change.diff.length}개 중 상위 30개):\n\`\`\`\n${diffLines.join("\n")}\n\`\`\``;
115
+ // Add structural signature changes
116
+ if (change.oldContent && change.newContent) {
117
+ const sigChanges = (0, context_extractor_1.diffSignatures)(change.oldContent, change.newContent, codeBlockLang);
118
+ if (sigChanges.length > 0) {
119
+ diffSummary += "\n\n### 구조적 변경 (시그니처 비교)";
120
+ for (const sc of sigChanges) {
121
+ if (sc.type === "added") {
122
+ diffSummary += `\n+ 추가: ${sc.newSignature}`;
123
+ }
124
+ else if (sc.type === "removed") {
125
+ diffSummary += `\n- 삭제: ${sc.oldSignature}`;
126
+ }
127
+ else {
128
+ diffSummary += `\n~ 변경: ${sc.oldSignature}\n → ${sc.newSignature}`;
129
+ }
130
+ }
131
+ }
132
+ }
122
133
  if (change.newContent) {
123
- diffSummary += `\n\n전체 수정 후 파일:\n\`\`\`${codeBlockLang}\n${change.newContent.split("\n").slice(0, 80).join("\n")}\n\`\`\``;
134
+ const smartNew = (0, context_extractor_1.buildSmartContext)(change.newContent, codeBlockLang);
135
+ diffSummary += `\n\n전체 수정 후 파일:\n\`\`\`${codeBlockLang}\n${smartNew}\n\`\`\``;
124
136
  }
125
137
  }
126
138
  const userPrompt = `## 파일 변경 감지: ${relPath}
@@ -134,19 +146,11 @@ ${connectedFiles.length > 0 ? `## 연결된 파일들 (${connectedFiles.length}
134
146
 
135
147
  이 변경이 프로젝트에 미치는 영향을 분석하세요.`;
136
148
  try {
137
- const ai = getGenAI();
138
- const model = ai.getGenerativeModel({ model: "gemini-2.5-flash" });
139
- const result = await model.generateContent({
140
- contents: [{ role: "user", parts: [{ text: userPrompt }] }],
141
- systemInstruction: { role: "model", parts: [{ text: REALTIME_SYSTEM_PROMPT }] },
142
- });
143
- const text = result.response.text();
144
- let jsonStr = text;
145
- const jsonMatch = text.match(/```(?:json)?\s*([\s\S]*?)```/);
146
- if (jsonMatch)
147
- jsonStr = jsonMatch[1];
148
- jsonStr = jsonStr.trim();
149
- const parsed = JSON.parse(jsonStr);
149
+ const provider = (0, provider_1.getAIProvider)();
150
+ if (!provider) {
151
+ throw new Error("No AI provider available (set GEMINI_KEY, OPENAI_KEY, or ANTHROPIC_KEY)");
152
+ }
153
+ const parsed = await provider.analyzeJSON(REALTIME_SYSTEM_PROMPT, userPrompt);
150
154
  const analysisMs = Date.now() - start;
151
155
  return {
152
156
  file: relPath,
package/dist/index.js CHANGED
@@ -51,6 +51,7 @@ const plugin_1 = require("./languages/plugin");
51
51
  const analyze_impact_1 = require("./tools/analyze-impact");
52
52
  const gate_build_1 = require("./tools/gate-build");
53
53
  const analyzer_1 = require("./ai/analyzer");
54
+ const provider_1 = require("./ai/provider");
54
55
  const server_1 = require("./web/server");
55
56
  const file_cache_1 = require("./watcher/file-cache");
56
57
  const validator_1 = require("./license/validator");
@@ -110,7 +111,7 @@ async function main() {
110
111
  };
111
112
  process.on("SIGINT", shutdown);
112
113
  process.on("SIGTERM", shutdown);
113
- const server = new index_js_1.Server({ name: "syke", version: "0.4.0" }, { capabilities: { tools: {} } });
114
+ const server = new index_js_1.Server({ name: "syke", version: "1.3.0" }, { capabilities: { tools: {} } });
114
115
  // List tools
115
116
  server.setRequestHandler(types_js_1.ListToolsRequestSchema, async () => ({
116
117
  tools: [
@@ -190,7 +191,7 @@ async function main() {
190
191
  },
191
192
  {
192
193
  name: "ai_analyze",
193
- description: "Use Gemini AI 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.",
194
+ 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.",
194
195
  inputSchema: {
195
196
  type: "object",
196
197
  properties: {
@@ -460,8 +461,16 @@ async function main() {
460
461
  }
461
462
  });
462
463
  // Pre-warm the graph (skip if no project root — e.g. Smithery scan)
463
- console.error(`[syke] Starting SYKE MCP Server v0.4.0`);
464
+ console.error(`[syke] Starting SYKE MCP Server v1.3.0`);
464
465
  console.error(`[syke] License: ${licenseStatus.plan.toUpperCase()} (${licenseStatus.source})`);
466
+ // Log AI provider status
467
+ const aiProvider = (0, provider_1.getAIProvider)();
468
+ if (aiProvider) {
469
+ console.error(`[syke] AI Provider: ${(0, provider_1.getProviderName)()}`);
470
+ }
471
+ else {
472
+ console.error(`[syke] AI: disabled (set GEMINI_KEY, OPENAI_KEY, or ANTHROPIC_KEY)`);
473
+ }
465
474
  if (licenseStatus.plan === "pro") {
466
475
  console.error(`[syke] Pro activated for: ${licenseStatus.email || "unknown"}`);
467
476
  }
@@ -544,7 +553,7 @@ main().catch((err) => {
544
553
  * See: https://smithery.ai/docs/deploy#sandbox-server
545
554
  */
546
555
  function createSandboxServer() {
547
- const sandboxServer = new index_js_1.Server({ name: "syke", version: "0.4.0" }, { capabilities: { tools: {} } });
556
+ const sandboxServer = new index_js_1.Server({ name: "syke", version: "1.3.0" }, { capabilities: { tools: {} } });
548
557
  sandboxServer.setRequestHandler(types_js_1.ListToolsRequestSchema, async () => ({
549
558
  tools: [
550
559
  {
@@ -579,7 +588,7 @@ function createSandboxServer() {
579
588
  },
580
589
  {
581
590
  name: "ai_analyze",
582
- description: "Pro: Gemini AI semantic analysis of a file and its dependents.",
591
+ description: "Pro: AI semantic analysis (Gemini/OpenAI/Claude) of a file and its dependents.",
583
592
  inputSchema: { type: "object", properties: { file: { type: "string", description: "File to analyze" } }, required: ["file"] },
584
593
  },
585
594
  {
package/package.json CHANGED
@@ -1,6 +1,7 @@
1
1
  {
2
2
  "name": "@syke1/mcp-server",
3
- "version": "1.2.0",
3
+ "version": "1.3.0",
4
+ "mcpName": "io.github.khalomsky/syke",
4
5
  "description": "AI code impact analysis MCP server — dependency graphs, cascade detection, and a mandatory build gate for AI coding agents",
5
6
  "main": "dist/index.js",
6
7
  "bin": {
package/smithery.yaml CHANGED
@@ -13,6 +13,18 @@ startCommand:
13
13
  type: string
14
14
  default: ""
15
15
  description: Google Gemini API key for AI semantic analysis (Pro only, optional)
16
+ openaiKey:
17
+ type: string
18
+ default: ""
19
+ description: OpenAI API key for AI semantic analysis (Pro only, alternative to Gemini)
20
+ anthropicKey:
21
+ type: string
22
+ default: ""
23
+ description: Anthropic API key for AI semantic analysis (Pro only, alternative to Gemini)
24
+ aiProvider:
25
+ type: string
26
+ default: ""
27
+ description: Force a specific AI provider (gemini, openai, or anthropic). Auto-selects if empty.
16
28
  commandFunction:
17
29
  |-
18
- config => ({ command: 'npx', args: ['-y', '@syke1/mcp-server@latest'], env: { SYKE_LICENSE_KEY: config.licenseKey || '', GEMINI_KEY: config.geminiKey || '', SYKE_NO_BROWSER: '1' } })
30
+ config => ({ command: 'npx', args: ['-y', '@syke1/mcp-server@latest'], env: { SYKE_LICENSE_KEY: config.licenseKey || '', GEMINI_KEY: config.geminiKey || '', OPENAI_KEY: config.openaiKey || '', ANTHROPIC_KEY: config.anthropicKey || '', SYKE_AI_PROVIDER: config.aiProvider || '', SYKE_NO_BROWSER: '1' } })