@syke1/mcp-server 1.4.5 → 1.4.7

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,17 +587,48 @@ 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, () => {
593
621
  const dashUrl = `http://localhost:${WEB_PORT}`;
594
622
  console.error(`[syke] Web dashboard: ${dashUrl}`);
595
623
  // Auto-open browser (disable with SYKE_NO_BROWSER=1)
624
+ // Delay 1s to let server fully stabilize before dashboard connects
596
625
  if (process.env.SYKE_NO_BROWSER !== "1") {
597
- const cmd = process.platform === "win32" ? `start ${dashUrl}`
598
- : process.platform === "darwin" ? `open ${dashUrl}`
599
- : `xdg-open ${dashUrl}`;
600
- (0, child_process_1.exec)(cmd, () => { });
626
+ setTimeout(() => {
627
+ const cmd = process.platform === "win32" ? `start ${dashUrl}`
628
+ : process.platform === "darwin" ? `open ${dashUrl}`
629
+ : `xdg-open ${dashUrl}`;
630
+ (0, child_process_1.exec)(cmd, () => { });
631
+ }, 1000);
601
632
  }
602
633
  });
603
634
  }
@@ -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();
@@ -2733,11 +2734,17 @@ window.syke = {
2733
2734
  let sseSource = null;
2734
2735
  let sseReconnectTimer = null;
2735
2736
  let sseBlocked = false;
2737
+ let sseEverConnected = false; // track if SSE has ever connected successfully
2736
2738
  const realtimeLog = []; // recent events for panel
2737
2739
 
2738
2740
  async function initSSE() {
2739
2741
  if (sseSource) { sseSource.close(); sseSource = null; }
2740
2742
 
2743
+ // Show appropriate status based on whether we've connected before
2744
+ if (!sseEverConnected) {
2745
+ updateSSEStatus("CONNECTING...", "warning");
2746
+ }
2747
+
2741
2748
  // Pre-check: if Free tier, SSE will 403 — don't attempt connection
2742
2749
  try {
2743
2750
  const probe = await fetch("/api/events");
@@ -2748,7 +2755,14 @@ async function initSSE() {
2748
2755
  }
2749
2756
  // Close the successful probe connection (we'll open EventSource next)
2750
2757
  if (probe.body) probe.body.cancel().catch(() => {});
2751
- } catch(e) { /* network error, try SSE anyway */ }
2758
+ } catch(e) {
2759
+ // Server not ready yet — retry after short delay on first attempt
2760
+ if (!sseEverConnected) {
2761
+ if (sseReconnectTimer) clearTimeout(sseReconnectTimer);
2762
+ sseReconnectTimer = setTimeout(() => { initSSE(); }, 2000);
2763
+ return;
2764
+ }
2765
+ }
2752
2766
 
2753
2767
  sseSource = new EventSource("/api/events");
2754
2768
 
@@ -2756,6 +2770,7 @@ async function initSSE() {
2756
2770
  const data = JSON.parse(e.data);
2757
2771
  console.log("[SYKE:SSE] Connected, cache:", data.cacheSize, "files");
2758
2772
  healthFailCount = 0; // Reset health failures on SSE connect
2773
+ sseEverConnected = true;
2759
2774
  updateSSEStatus("LIVE", "connected");
2760
2775
  });
2761
2776
 
@@ -2948,7 +2963,7 @@ async function initSSE() {
2948
2963
  if (sseBlocked) return;
2949
2964
 
2950
2965
  sseRetryCount++;
2951
- updateSSEStatus("RECONNECTING...", "warning");
2966
+ updateSSEStatus(sseEverConnected ? "RECONNECTING..." : "CONNECTING...", "warning");
2952
2967
 
2953
2968
  // Only show offline after 5 consecutive SSE failures
2954
2969
  if (sseRetryCount >= 5) {
@@ -3974,3 +3989,115 @@ function setupProjectModal() {
3974
3989
  if (e.target === modal) modal.classList.add("hidden");
3975
3990
  });
3976
3991
  }
3992
+
3993
+ // ══════════════════════════════════════════════════════════════
3994
+ // AI KEYS MODAL
3995
+ // ══════════════════════════════════════════════════════════════
3996
+ function setupAIKeysModal() {
3997
+ const btn = document.getElementById("btn-ai-keys");
3998
+ const modal = document.getElementById("ai-keys-modal");
3999
+ const closeBtn = document.getElementById("btn-ai-keys-close");
4000
+ const activeEl = document.getElementById("ai-keys-active");
4001
+ if (!btn || !modal) return;
4002
+
4003
+ function updateStatus(row, isConfigured, isActive) {
4004
+ const statusEl = row.querySelector(".ai-key-status");
4005
+ if (isActive) {
4006
+ statusEl.textContent = "ACTIVE";
4007
+ statusEl.className = "ai-key-status active";
4008
+ } else if (isConfigured) {
4009
+ statusEl.textContent = "CONFIGURED";
4010
+ statusEl.className = "ai-key-status configured";
4011
+ } else {
4012
+ statusEl.textContent = "---";
4013
+ statusEl.className = "ai-key-status none";
4014
+ }
4015
+ }
4016
+
4017
+ function updateAll(aiKeys, activeProvider) {
4018
+ const rows = modal.querySelectorAll(".ai-key-row");
4019
+ rows.forEach(function(row) {
4020
+ const provider = row.dataset.provider;
4021
+ const configured = aiKeys[provider] || false;
4022
+ const isActive = configured && activeProvider.toLowerCase().includes(provider);
4023
+ updateStatus(row, configured, isActive);
4024
+ });
4025
+ if (activeProvider && activeProvider !== "disabled") {
4026
+ activeEl.textContent = "Active: " + activeProvider;
4027
+ } else {
4028
+ activeEl.textContent = "No AI provider configured";
4029
+ activeEl.style.color = "var(--text-secondary)";
4030
+ }
4031
+ }
4032
+
4033
+ async function openModal() {
4034
+ modal.classList.remove("hidden");
4035
+ // Clear inputs
4036
+ modal.querySelectorAll(".ai-key-input").forEach(function(inp) { inp.value = ""; });
4037
+ // Fetch current state
4038
+ try {
4039
+ const res = await fetch("/api/project-info");
4040
+ const info = await res.json();
4041
+ updateAll(info.aiKeys || {}, info.aiProvider || "disabled");
4042
+ } catch {
4043
+ activeEl.textContent = "Failed to fetch status";
4044
+ }
4045
+ }
4046
+
4047
+ function closeModal() {
4048
+ modal.classList.add("hidden");
4049
+ }
4050
+
4051
+ btn.addEventListener("click", openModal);
4052
+ closeBtn.addEventListener("click", closeModal);
4053
+ modal.addEventListener("click", function(e) { if (e.target === modal) closeModal(); });
4054
+
4055
+ // SET button handlers
4056
+ modal.querySelectorAll(".ai-key-row").forEach(function(row) {
4057
+ const setBtn = row.querySelector(".ai-key-set-btn");
4058
+ const input = row.querySelector(".ai-key-input");
4059
+ const provider = row.dataset.provider;
4060
+
4061
+ setBtn.addEventListener("click", async function() {
4062
+ const key = input.value.trim();
4063
+ setBtn.disabled = true;
4064
+ setBtn.textContent = "...";
4065
+
4066
+ try {
4067
+ const res = await fetch("/api/set-ai-key", {
4068
+ method: "POST",
4069
+ headers: { "Content-Type": "application/json" },
4070
+ body: JSON.stringify({ provider: provider, key: key || null }),
4071
+ });
4072
+ const data = await res.json();
4073
+ if (data.success) {
4074
+ updateAll(data.configured, data.activeProvider);
4075
+ input.value = "";
4076
+ if (key) {
4077
+ input.placeholder = "****" + key.slice(-4);
4078
+ } else {
4079
+ var placeholders = { gemini: "AIzaSy...", openai: "sk-...", anthropic: "sk-ant-..." };
4080
+ input.placeholder = placeholders[provider] || "";
4081
+ }
4082
+ }
4083
+ } catch (err) {
4084
+ var statusEl = row.querySelector(".ai-key-status");
4085
+ statusEl.textContent = "ERROR";
4086
+ statusEl.className = "ai-key-status";
4087
+ statusEl.style.color = "#ff5f57";
4088
+ }
4089
+ setBtn.disabled = false;
4090
+ setBtn.textContent = "SET";
4091
+ });
4092
+
4093
+ input.addEventListener("keydown", function(e) {
4094
+ if (e.key === "Enter") setBtn.click();
4095
+ });
4096
+ });
4097
+
4098
+ document.addEventListener("keydown", function(e) {
4099
+ if (e.key === "Escape" && !modal.classList.contains("hidden")) {
4100
+ closeModal();
4101
+ }
4102
+ });
4103
+ }
@@ -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.7",
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",