@syke1/mcp-server 1.4.5 → 1.4.6

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.
@@ -15,6 +15,11 @@ export interface AIProvider {
15
15
  * 2. Auto-select: GEMINI_KEY > OPENAI_KEY > ANTHROPIC_KEY
16
16
  */
17
17
  export declare function getAIProvider(): AIProvider | null;
18
+ /**
19
+ * Reset the cached provider so the next getAIProvider() call re-evaluates config.
20
+ * Call this after changing API keys at runtime.
21
+ */
22
+ export declare function resetAIProvider(): void;
18
23
  /**
19
24
  * Human-readable name for the active AI provider (for logs/UI).
20
25
  */
@@ -5,6 +5,7 @@
5
5
  */
6
6
  Object.defineProperty(exports, "__esModule", { value: true });
7
7
  exports.getAIProvider = getAIProvider;
8
+ exports.resetAIProvider = resetAIProvider;
8
9
  exports.getProviderName = getProviderName;
9
10
  const generative_ai_1 = require("@google/generative-ai");
10
11
  const config_1 = require("../config");
@@ -169,6 +170,13 @@ function getAIProvider() {
169
170
  }
170
171
  return cachedProvider;
171
172
  }
173
+ /**
174
+ * Reset the cached provider so the next getAIProvider() call re-evaluates config.
175
+ * Call this after changing API keys at runtime.
176
+ */
177
+ function resetAIProvider() {
178
+ cachedProvider = undefined;
179
+ }
172
180
  /**
173
181
  * Human-readable name for the active AI provider (for logs/UI).
174
182
  */
package/dist/index.js CHANGED
@@ -587,6 +587,34 @@ async function main() {
587
587
  licenseStatus = { plan: "free", source: "default" };
588
588
  return { success: true, plan: "free" };
589
589
  }
590
+ },
591
+ // setAIKeyFn
592
+ (provider, key) => {
593
+ const map = { gemini: "geminiKey", openai: "openaiKey", anthropic: "anthropicKey" };
594
+ const configKey = map[provider];
595
+ if (configKey) {
596
+ (0, config_1.setConfig)(configKey, key);
597
+ (0, provider_1.resetAIProvider)();
598
+ }
599
+ const gemini = !!(0, config_1.getConfig)("geminiKey", "GEMINI_KEY");
600
+ const openai = !!(0, config_1.getConfig)("openaiKey", "OPENAI_KEY");
601
+ const anthropic = !!(0, config_1.getConfig)("anthropicKey", "ANTHROPIC_KEY");
602
+ return {
603
+ success: true,
604
+ activeProvider: (0, provider_1.getProviderName)(),
605
+ configured: { gemini, openai, anthropic },
606
+ };
607
+ },
608
+ // getAIInfoFn
609
+ () => {
610
+ return {
611
+ activeProvider: (0, provider_1.getProviderName)(),
612
+ configured: {
613
+ gemini: !!(0, config_1.getConfig)("geminiKey", "GEMINI_KEY"),
614
+ openai: !!(0, config_1.getConfig)("openaiKey", "OPENAI_KEY"),
615
+ anthropic: !!(0, config_1.getConfig)("anthropicKey", "ANTHROPIC_KEY"),
616
+ },
617
+ };
590
618
  });
591
619
  webServerHandle = { setFileCache: setWebFileCache };
592
620
  webApp.listen(WEB_PORT, () => {
@@ -464,6 +464,7 @@ document.addEventListener("DOMContentLoaded", async () => {
464
464
  setupSettings();
465
465
  setupProjectModal();
466
466
  setupLicenseModal();
467
+ setupAIKeysModal();
467
468
  setupFileTree();
468
469
  initSSE();
469
470
  startHealthCheck();
@@ -3974,3 +3975,115 @@ function setupProjectModal() {
3974
3975
  if (e.target === modal) modal.classList.add("hidden");
3975
3976
  });
3976
3977
  }
3978
+
3979
+ // ══════════════════════════════════════════════════════════════
3980
+ // AI KEYS MODAL
3981
+ // ══════════════════════════════════════════════════════════════
3982
+ function setupAIKeysModal() {
3983
+ const btn = document.getElementById("btn-ai-keys");
3984
+ const modal = document.getElementById("ai-keys-modal");
3985
+ const closeBtn = document.getElementById("btn-ai-keys-close");
3986
+ const activeEl = document.getElementById("ai-keys-active");
3987
+ if (!btn || !modal) return;
3988
+
3989
+ function updateStatus(row, isConfigured, isActive) {
3990
+ const statusEl = row.querySelector(".ai-key-status");
3991
+ if (isActive) {
3992
+ statusEl.textContent = "ACTIVE";
3993
+ statusEl.className = "ai-key-status active";
3994
+ } else if (isConfigured) {
3995
+ statusEl.textContent = "CONFIGURED";
3996
+ statusEl.className = "ai-key-status configured";
3997
+ } else {
3998
+ statusEl.textContent = "---";
3999
+ statusEl.className = "ai-key-status none";
4000
+ }
4001
+ }
4002
+
4003
+ function updateAll(aiKeys, activeProvider) {
4004
+ const rows = modal.querySelectorAll(".ai-key-row");
4005
+ rows.forEach(function(row) {
4006
+ const provider = row.dataset.provider;
4007
+ const configured = aiKeys[provider] || false;
4008
+ const isActive = configured && activeProvider.toLowerCase().includes(provider);
4009
+ updateStatus(row, configured, isActive);
4010
+ });
4011
+ if (activeProvider && activeProvider !== "disabled") {
4012
+ activeEl.textContent = "Active: " + activeProvider;
4013
+ } else {
4014
+ activeEl.textContent = "No AI provider configured";
4015
+ activeEl.style.color = "var(--text-secondary)";
4016
+ }
4017
+ }
4018
+
4019
+ async function openModal() {
4020
+ modal.classList.remove("hidden");
4021
+ // Clear inputs
4022
+ modal.querySelectorAll(".ai-key-input").forEach(function(inp) { inp.value = ""; });
4023
+ // Fetch current state
4024
+ try {
4025
+ const res = await fetch("/api/project-info");
4026
+ const info = await res.json();
4027
+ updateAll(info.aiKeys || {}, info.aiProvider || "disabled");
4028
+ } catch {
4029
+ activeEl.textContent = "Failed to fetch status";
4030
+ }
4031
+ }
4032
+
4033
+ function closeModal() {
4034
+ modal.classList.add("hidden");
4035
+ }
4036
+
4037
+ btn.addEventListener("click", openModal);
4038
+ closeBtn.addEventListener("click", closeModal);
4039
+ modal.addEventListener("click", function(e) { if (e.target === modal) closeModal(); });
4040
+
4041
+ // SET button handlers
4042
+ modal.querySelectorAll(".ai-key-row").forEach(function(row) {
4043
+ const setBtn = row.querySelector(".ai-key-set-btn");
4044
+ const input = row.querySelector(".ai-key-input");
4045
+ const provider = row.dataset.provider;
4046
+
4047
+ setBtn.addEventListener("click", async function() {
4048
+ const key = input.value.trim();
4049
+ setBtn.disabled = true;
4050
+ setBtn.textContent = "...";
4051
+
4052
+ try {
4053
+ const res = await fetch("/api/set-ai-key", {
4054
+ method: "POST",
4055
+ headers: { "Content-Type": "application/json" },
4056
+ body: JSON.stringify({ provider: provider, key: key || null }),
4057
+ });
4058
+ const data = await res.json();
4059
+ if (data.success) {
4060
+ updateAll(data.configured, data.activeProvider);
4061
+ input.value = "";
4062
+ if (key) {
4063
+ input.placeholder = "****" + key.slice(-4);
4064
+ } else {
4065
+ var placeholders = { gemini: "AIzaSy...", openai: "sk-...", anthropic: "sk-ant-..." };
4066
+ input.placeholder = placeholders[provider] || "";
4067
+ }
4068
+ }
4069
+ } catch (err) {
4070
+ var statusEl = row.querySelector(".ai-key-status");
4071
+ statusEl.textContent = "ERROR";
4072
+ statusEl.className = "ai-key-status";
4073
+ statusEl.style.color = "#ff5f57";
4074
+ }
4075
+ setBtn.disabled = false;
4076
+ setBtn.textContent = "SET";
4077
+ });
4078
+
4079
+ input.addEventListener("keydown", function(e) {
4080
+ if (e.key === "Enter") setBtn.click();
4081
+ });
4082
+ });
4083
+
4084
+ document.addEventListener("keydown", function(e) {
4085
+ if (e.key === "Escape" && !modal.classList.contains("hidden")) {
4086
+ closeModal();
4087
+ }
4088
+ });
4089
+ }
@@ -21,6 +21,7 @@
21
21
  <span id="sse-status" class="sse-indicator offline">OFFLINE</span>
22
22
  <span id="license-badge" class="license-badge free">FREE</span>
23
23
  <button id="btn-license" class="top-btn license-btn">LICENSE</button>
24
+ <button id="btn-ai-keys" class="top-btn ai-keys-btn">AI</button>
24
25
  </div>
25
26
  <div class="project-selector">
26
27
  <span id="current-project" class="project-path">Loading...</span>
@@ -455,6 +456,39 @@
455
456
  </div>
456
457
  </div>
457
458
 
459
+ <!-- AI Keys Modal -->
460
+ <div id="ai-keys-modal" class="hidden">
461
+ <div class="ai-keys-modal-panel">
462
+ <h3>AI PROVIDER KEYS</h3>
463
+ <p class="ai-keys-modal-desc">Configure API keys to enable AI analysis.</p>
464
+ <div class="ai-keys-rows">
465
+ <div class="ai-key-row" data-provider="gemini">
466
+ <span class="ai-key-label">GEMINI</span>
467
+ <input type="password" class="ai-key-input" placeholder="AIzaSy..." spellcheck="false" autocomplete="off">
468
+ <button class="ai-key-set-btn">SET</button>
469
+ <span class="ai-key-status"></span>
470
+ </div>
471
+ <div class="ai-key-row" data-provider="openai">
472
+ <span class="ai-key-label">OPENAI</span>
473
+ <input type="password" class="ai-key-input" placeholder="sk-..." spellcheck="false" autocomplete="off">
474
+ <button class="ai-key-set-btn">SET</button>
475
+ <span class="ai-key-status"></span>
476
+ </div>
477
+ <div class="ai-key-row" data-provider="anthropic">
478
+ <span class="ai-key-label">ANTHROPIC</span>
479
+ <input type="password" class="ai-key-input" placeholder="sk-ant-..." spellcheck="false" autocomplete="off">
480
+ <button class="ai-key-set-btn">SET</button>
481
+ <span class="ai-key-status"></span>
482
+ </div>
483
+ </div>
484
+ <div id="ai-keys-active" class="ai-keys-active"></div>
485
+ <div class="ai-keys-modal-actions">
486
+ <button id="btn-ai-keys-close">CLOSE</button>
487
+ </div>
488
+ <p class="ai-keys-priority">Priority: Gemini &gt; OpenAI &gt; Anthropic</p>
489
+ </div>
490
+ </div>
491
+
458
492
  <!-- Bottom status bar -->
459
493
  <div id="bottom-bar">
460
494
  <span id="bottom-info">SYKE v--- · ---</span>
@@ -2060,6 +2060,154 @@ main {
2060
2060
  letter-spacing: 1.5px;
2061
2061
  }
2062
2062
 
2063
+ .ai-keys-btn {
2064
+ font-size: 9px !important;
2065
+ padding: 2px 8px !important;
2066
+ letter-spacing: 1.5px;
2067
+ }
2068
+
2069
+ /* ═══════════════════════════════════════════ */
2070
+ /* AI Keys Modal */
2071
+ /* ═══════════════════════════════════════════ */
2072
+ #ai-keys-modal {
2073
+ position: fixed;
2074
+ inset: 0;
2075
+ background: rgba(5,10,24,0.85);
2076
+ z-index: 400;
2077
+ display: flex;
2078
+ align-items: center;
2079
+ justify-content: center;
2080
+ backdrop-filter: blur(4px);
2081
+ }
2082
+ #ai-keys-modal.hidden { display: none; }
2083
+
2084
+ .ai-keys-modal-panel {
2085
+ background: var(--bg-secondary);
2086
+ border: 1px solid var(--accent-dim);
2087
+ border-radius: 6px;
2088
+ padding: 24px 32px;
2089
+ min-width: 460px;
2090
+ max-width: 520px;
2091
+ box-shadow: var(--glow-cyan), 0 16px 64px rgba(0,0,0,0.5);
2092
+ }
2093
+ .ai-keys-modal-panel h3 {
2094
+ font-size: 12px;
2095
+ color: var(--accent);
2096
+ letter-spacing: 3px;
2097
+ margin-bottom: 8px;
2098
+ }
2099
+ .ai-keys-modal-desc {
2100
+ font-size: 11px;
2101
+ color: var(--text-secondary);
2102
+ margin-bottom: 16px;
2103
+ }
2104
+ .ai-keys-rows {
2105
+ display: flex;
2106
+ flex-direction: column;
2107
+ gap: 8px;
2108
+ margin-bottom: 14px;
2109
+ }
2110
+ .ai-key-row {
2111
+ display: flex;
2112
+ align-items: center;
2113
+ gap: 8px;
2114
+ }
2115
+ .ai-key-label {
2116
+ font-size: 10px;
2117
+ letter-spacing: 2px;
2118
+ color: var(--text-secondary);
2119
+ width: 80px;
2120
+ flex-shrink: 0;
2121
+ }
2122
+ .ai-key-input {
2123
+ flex: 1;
2124
+ padding: 7px 10px;
2125
+ background: rgba(0,0,0,0.5);
2126
+ border: 1px solid var(--border);
2127
+ border-radius: 3px;
2128
+ color: var(--text-primary);
2129
+ font-family: inherit;
2130
+ font-size: 12px;
2131
+ letter-spacing: 1px;
2132
+ outline: none;
2133
+ transition: border-color 0.2s;
2134
+ }
2135
+ .ai-key-input:focus {
2136
+ border-color: var(--accent);
2137
+ box-shadow: 0 0 8px rgba(0,212,255,0.15);
2138
+ }
2139
+ .ai-key-input::placeholder {
2140
+ color: var(--text-secondary);
2141
+ font-size: 11px;
2142
+ opacity: 0.5;
2143
+ }
2144
+ .ai-key-set-btn {
2145
+ padding: 6px 12px;
2146
+ border: 1px solid var(--accent-dim);
2147
+ border-radius: 3px;
2148
+ background: transparent;
2149
+ color: var(--accent);
2150
+ font-family: inherit;
2151
+ font-size: 10px;
2152
+ letter-spacing: 2px;
2153
+ cursor: pointer;
2154
+ transition: all 0.2s;
2155
+ flex-shrink: 0;
2156
+ }
2157
+ .ai-key-set-btn:hover {
2158
+ background: rgba(0,212,255,0.08);
2159
+ border-color: var(--accent);
2160
+ }
2161
+ .ai-key-status {
2162
+ font-size: 9px;
2163
+ letter-spacing: 1px;
2164
+ width: 70px;
2165
+ text-align: center;
2166
+ flex-shrink: 0;
2167
+ }
2168
+ .ai-key-status.active {
2169
+ color: var(--risk-low);
2170
+ }
2171
+ .ai-key-status.configured {
2172
+ color: var(--text-secondary);
2173
+ }
2174
+ .ai-key-status.none {
2175
+ color: var(--text-secondary);
2176
+ opacity: 0.4;
2177
+ }
2178
+ .ai-keys-active {
2179
+ font-size: 11px;
2180
+ color: var(--risk-low);
2181
+ margin-bottom: 14px;
2182
+ min-height: 18px;
2183
+ }
2184
+ .ai-keys-modal-actions {
2185
+ display: flex;
2186
+ gap: 8px;
2187
+ margin-bottom: 10px;
2188
+ }
2189
+ .ai-keys-modal-actions button {
2190
+ padding: 8px 18px;
2191
+ border: 1px solid var(--accent-dim);
2192
+ border-radius: 3px;
2193
+ background: transparent;
2194
+ color: var(--accent);
2195
+ font-family: inherit;
2196
+ font-size: 10px;
2197
+ letter-spacing: 2px;
2198
+ cursor: pointer;
2199
+ transition: all 0.2s;
2200
+ }
2201
+ .ai-keys-modal-actions button:hover {
2202
+ background: rgba(0,212,255,0.08);
2203
+ border-color: var(--accent);
2204
+ }
2205
+ .ai-keys-priority {
2206
+ font-size: 9px;
2207
+ color: var(--text-secondary);
2208
+ opacity: 0.6;
2209
+ }
2210
+
2063
2211
  /* ═══════════════════════════════════════════ */
2064
2212
  /* Project Switch Modal */
2065
2213
  /* ═══════════════════════════════════════════ */
@@ -40,4 +40,19 @@ export declare function createWebServer(getGraphFn: () => DependencyGraph, initi
40
40
  plan?: string;
41
41
  expiresAt?: string;
42
42
  error?: string;
43
- }>): WebServerHandle;
43
+ }>, setAIKeyFn?: (provider: string, key: string | null) => {
44
+ success: boolean;
45
+ activeProvider: string;
46
+ configured: {
47
+ gemini: boolean;
48
+ openai: boolean;
49
+ anthropic: boolean;
50
+ };
51
+ }, getAIInfoFn?: () => {
52
+ activeProvider: string;
53
+ configured: {
54
+ gemini: boolean;
55
+ openai: boolean;
56
+ anthropic: boolean;
57
+ };
58
+ }): WebServerHandle;
@@ -227,7 +227,7 @@ function acknowledgeWarnings() {
227
227
  function getAllWarnings() {
228
228
  return [...warningStore];
229
229
  }
230
- function createWebServer(getGraphFn, initialFileCache, switchProjectFn, getProjectRoot, getPackageName, getLicenseStatus, hasAIKeyFn, setLicenseKeyFn) {
230
+ function createWebServer(getGraphFn, initialFileCache, switchProjectFn, getProjectRoot, getPackageName, getLicenseStatus, hasAIKeyFn, setLicenseKeyFn, setAIKeyFn, getAIInfoFn) {
231
231
  const app = (0, express_1.default)();
232
232
  app.use(express_1.default.json());
233
233
  // Serve static files from public/
@@ -745,6 +745,7 @@ function createWebServer(getGraphFn, initialFileCache, switchProjectFn, getProje
745
745
  const maskedKey = rawKey.length > 10
746
746
  ? rawKey.substring(0, 9) + "····" + rawKey.substring(rawKey.length - 4)
747
747
  : "";
748
+ const aiInfo = getAIInfoFn ? getAIInfoFn() : { activeProvider: "disabled", configured: { gemini: false, openai: false, anthropic: false } };
748
749
  res.json({
749
750
  projectRoot: getProjectRoot ? getProjectRoot() : graph.projectRoot,
750
751
  packageName: getPackageName ? getPackageName() : "",
@@ -757,6 +758,8 @@ function createWebServer(getGraphFn, initialFileCache, switchProjectFn, getProje
757
758
  licenseKey: maskedKey,
758
759
  freeFileLimit: 50,
759
760
  sykeVersion,
761
+ aiProvider: aiInfo.activeProvider,
762
+ aiKeys: aiInfo.configured,
760
763
  });
761
764
  });
762
765
  // POST /api/set-license-key — Set or remove license key via dashboard
@@ -774,6 +777,25 @@ function createWebServer(getGraphFn, initialFileCache, switchProjectFn, getProje
774
777
  res.status(500).json({ success: false, error: err.message || "Unknown error" });
775
778
  }
776
779
  });
780
+ // POST /api/set-ai-key — Set or remove an AI provider API key
781
+ app.post("/api/set-ai-key", (req, res) => {
782
+ if (!setAIKeyFn) {
783
+ res.status(501).json({ success: false, error: "Not supported" });
784
+ return;
785
+ }
786
+ const { provider, key } = req.body;
787
+ if (!provider || !["gemini", "openai", "anthropic"].includes(provider)) {
788
+ res.status(400).json({ success: false, error: "provider must be gemini, openai, or anthropic" });
789
+ return;
790
+ }
791
+ try {
792
+ const result = setAIKeyFn(provider, key || null);
793
+ res.json(result);
794
+ }
795
+ catch (err) {
796
+ res.status(500).json({ success: false, error: err.message || "Unknown error" });
797
+ }
798
+ });
777
799
  // GET /api/browse-dirs — List subdirectories for folder browser
778
800
  app.get("/api/browse-dirs", (req, res) => {
779
801
  const dirPath = req.query.path || (process.platform === "win32" ? "C:\\" : "/");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@syke1/mcp-server",
3
- "version": "1.4.5",
3
+ "version": "1.4.6",
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",