@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 +29 -6
- package/dist/ai/analyzer.js +13 -21
- package/dist/ai/context-extractor.d.ts +32 -0
- package/dist/ai/context-extractor.js +224 -0
- package/dist/ai/provider.d.ts +21 -0
- package/dist/ai/provider.js +174 -0
- package/dist/ai/realtime-analyzer.d.ts +1 -1
- package/dist/ai/realtime-analyzer.js +35 -31
- package/dist/index.js +14 -5
- package/package.json +2 -1
- package/smithery.yaml +13 -1
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** —
|
|
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
|
|
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` |
|
|
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 |
|
package/dist/ai/analyzer.js
CHANGED
|
@@ -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
|
|
40
|
-
|
|
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
|
|
82
|
-
|
|
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
|
-
|
|
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
|
-
${
|
|
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
|
-
|
|
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
|
|
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
|
|
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'
|
|
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
|
|
102
|
-
connectedFiles.push(`### ${depRel}\n\`\`\`${codeBlockLang}\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
|
-
|
|
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
|
-
|
|
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
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
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: "
|
|
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
|
|
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
|
|
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: "
|
|
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:
|
|
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.
|
|
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' } })
|