@syke1/mcp-server 1.3.4 → 1.3.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.
@@ -7,6 +7,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
7
7
  exports.getAIProvider = getAIProvider;
8
8
  exports.getProviderName = getProviderName;
9
9
  const generative_ai_1 = require("@google/generative-ai");
10
+ const config_1 = require("../config");
10
11
  // ── Gemini ──────────────────────────────────────────────────────────
11
12
  class GeminiProvider {
12
13
  constructor(apiKey) {
@@ -133,16 +134,19 @@ let cachedProvider = undefined;
133
134
  function getAIProvider() {
134
135
  if (cachedProvider !== undefined)
135
136
  return cachedProvider;
136
- const forced = process.env.SYKE_AI_PROVIDER?.toLowerCase();
137
+ const forced = (0, config_1.getConfig)("aiProvider", "SYKE_AI_PROVIDER")?.toLowerCase();
138
+ const geminiKey = (0, config_1.getConfig)("geminiKey", "GEMINI_KEY");
139
+ const openaiKey = (0, config_1.getConfig)("openaiKey", "OPENAI_KEY");
140
+ const anthropicKey = (0, config_1.getConfig)("anthropicKey", "ANTHROPIC_KEY");
137
141
  if (forced) {
138
- if (forced === "gemini" && process.env.GEMINI_KEY) {
139
- cachedProvider = new GeminiProvider(process.env.GEMINI_KEY);
142
+ if (forced === "gemini" && geminiKey) {
143
+ cachedProvider = new GeminiProvider(geminiKey);
140
144
  }
141
- else if (forced === "openai" && process.env.OPENAI_KEY) {
142
- cachedProvider = new OpenAIProvider(process.env.OPENAI_KEY);
145
+ else if (forced === "openai" && openaiKey) {
146
+ cachedProvider = new OpenAIProvider(openaiKey);
143
147
  }
144
- else if (forced === "anthropic" && process.env.ANTHROPIC_KEY) {
145
- cachedProvider = new AnthropicProvider(process.env.ANTHROPIC_KEY);
148
+ else if (forced === "anthropic" && anthropicKey) {
149
+ cachedProvider = new AnthropicProvider(anthropicKey);
146
150
  }
147
151
  else {
148
152
  console.error(`[syke] SYKE_AI_PROVIDER=${forced} but no matching API key found`);
@@ -151,14 +155,14 @@ function getAIProvider() {
151
155
  return cachedProvider;
152
156
  }
153
157
  // Auto-select
154
- if (process.env.GEMINI_KEY) {
155
- cachedProvider = new GeminiProvider(process.env.GEMINI_KEY);
158
+ if (geminiKey) {
159
+ cachedProvider = new GeminiProvider(geminiKey);
156
160
  }
157
- else if (process.env.OPENAI_KEY) {
158
- cachedProvider = new OpenAIProvider(process.env.OPENAI_KEY);
161
+ else if (openaiKey) {
162
+ cachedProvider = new OpenAIProvider(openaiKey);
159
163
  }
160
- else if (process.env.ANTHROPIC_KEY) {
161
- cachedProvider = new AnthropicProvider(process.env.ANTHROPIC_KEY);
164
+ else if (anthropicKey) {
165
+ cachedProvider = new AnthropicProvider(anthropicKey);
162
166
  }
163
167
  else {
164
168
  cachedProvider = null;
@@ -0,0 +1,19 @@
1
+ interface SykeConfig {
2
+ licenseKey?: string;
3
+ geminiKey?: string;
4
+ openaiKey?: string;
5
+ anthropicKey?: string;
6
+ aiProvider?: string;
7
+ port?: number;
8
+ }
9
+ /**
10
+ * Get a config value. Env var takes priority over config file.
11
+ */
12
+ export declare function getConfig(key: keyof SykeConfig, envVar?: string): string | undefined;
13
+ /**
14
+ * Get all resolved config (for logging/debug)
15
+ */
16
+ export declare function getAllConfig(): Record<string, string | undefined>;
17
+ export declare const CONFIG_DIR_PATH: string;
18
+ export declare const CONFIG_FILE_PATH: string;
19
+ export {};
package/dist/config.js ADDED
@@ -0,0 +1,98 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.CONFIG_FILE_PATH = exports.CONFIG_DIR_PATH = void 0;
37
+ exports.getConfig = getConfig;
38
+ exports.getAllConfig = getAllConfig;
39
+ /**
40
+ * Central config reader for SYKE MCP Server.
41
+ *
42
+ * Priority: environment variables > ~/.syke/config.json
43
+ *
44
+ * IDE users (Cursor, Windsurf, etc.) set env vars in their MCP config.
45
+ * Terminal users (Claude Code CLI) edit ~/.syke/config.json directly.
46
+ */
47
+ const fs = __importStar(require("fs"));
48
+ const path = __importStar(require("path"));
49
+ const os = __importStar(require("os"));
50
+ const CONFIG_DIR = path.join(os.homedir(), ".syke");
51
+ const CONFIG_FILE = path.join(CONFIG_DIR, "config.json");
52
+ let cached = null;
53
+ /**
54
+ * Read ~/.syke/config.json (cached after first read)
55
+ */
56
+ function readConfigFile() {
57
+ if (cached)
58
+ return cached;
59
+ try {
60
+ if (fs.existsSync(CONFIG_FILE)) {
61
+ cached = JSON.parse(fs.readFileSync(CONFIG_FILE, "utf-8"));
62
+ return cached;
63
+ }
64
+ }
65
+ catch {
66
+ // ignore parse errors
67
+ }
68
+ cached = {};
69
+ return cached;
70
+ }
71
+ /**
72
+ * Get a config value. Env var takes priority over config file.
73
+ */
74
+ function getConfig(key, envVar) {
75
+ // 1. Environment variable
76
+ if (envVar && process.env[envVar]) {
77
+ return process.env[envVar];
78
+ }
79
+ // 2. Config file
80
+ const file = readConfigFile();
81
+ const val = file[key];
82
+ return val !== undefined && val !== null ? String(val) : undefined;
83
+ }
84
+ /**
85
+ * Get all resolved config (for logging/debug)
86
+ */
87
+ function getAllConfig() {
88
+ return {
89
+ licenseKey: getConfig("licenseKey", "SYKE_LICENSE_KEY"),
90
+ geminiKey: getConfig("geminiKey", "GEMINI_KEY"),
91
+ openaiKey: getConfig("openaiKey", "OPENAI_KEY"),
92
+ anthropicKey: getConfig("anthropicKey", "ANTHROPIC_KEY"),
93
+ aiProvider: getConfig("aiProvider", "SYKE_AI_PROVIDER"),
94
+ port: getConfig("port", "SYKE_WEB_PORT"),
95
+ };
96
+ }
97
+ exports.CONFIG_DIR_PATH = CONFIG_DIR;
98
+ exports.CONFIG_FILE_PATH = CONFIG_FILE;
package/dist/graph.js CHANGED
@@ -48,10 +48,12 @@ function buildGraph(projectRoot, packageName) {
48
48
  const allSourceDirs = [];
49
49
  for (const plugin of detectedPlugins) {
50
50
  const dirs = plugin.getSourceDirs(projectRoot);
51
+ console.error(`[syke:debug] ${plugin.id} getSourceDirs(${projectRoot}) => ${dirs.length} dirs: ${dirs.join(", ")}`);
51
52
  for (const dir of dirs) {
52
53
  if (!allSourceDirs.includes(dir))
53
54
  allSourceDirs.push(dir);
54
55
  const sourceFiles = plugin.discoverFiles(dir);
56
+ console.error(`[syke:debug] ${plugin.id} discoverFiles(${dir}) => ${sourceFiles.length} files`);
55
57
  for (const f of sourceFiles) {
56
58
  files.add(f);
57
59
  if (!forward.has(f))
package/dist/index.js CHANGED
@@ -55,10 +55,11 @@ const provider_1 = require("./ai/provider");
55
55
  const server_1 = require("./web/server");
56
56
  const file_cache_1 = require("./watcher/file-cache");
57
57
  const validator_1 = require("./license/validator");
58
+ const config_1 = require("./config");
58
59
  // Configuration — auto-detect if env vars not set
59
60
  let currentProjectRoot = process.env.SYKE_currentProjectRoot || (0, plugin_1.detectProjectRoot)();
60
61
  let currentPackageName = process.env.SYKE_currentPackageName || (0, plugin_1.detectPackageName)(currentProjectRoot, (0, plugin_1.detectLanguages)(currentProjectRoot));
61
- const WEB_PORT = parseInt(process.env.SYKE_WEB_PORT || "3333", 10);
62
+ const WEB_PORT = parseInt((0, config_1.getConfig)("port", "SYKE_WEB_PORT") || "3333", 10);
62
63
  function resolveFilePath(fileArg, projectRoot, sourceDir) {
63
64
  const srcDir = sourceDir || path.join(projectRoot, "src");
64
65
  const srcDirName = path.basename(srcDir); // "lib" or "src"
@@ -120,7 +121,7 @@ async function main() {
120
121
  };
121
122
  process.on("SIGINT", shutdown);
122
123
  process.on("SIGTERM", shutdown);
123
- const server = new index_js_1.Server({ name: "syke", version: "1.3.4" }, { capabilities: { tools: {} } });
124
+ const server = new index_js_1.Server({ name: "syke", version: "1.3.5" }, { capabilities: { tools: {} } });
124
125
  // List tools
125
126
  server.setRequestHandler(types_js_1.ListToolsRequestSchema, async () => ({
126
127
  tools: [
@@ -389,10 +390,11 @@ async function main() {
389
390
  };
390
391
  }
391
392
  case "ai_analyze": {
392
- // Pro-only feature
393
- if (licenseStatus.plan !== "pro") {
393
+ // BYOK: allow if user has their own AI key, even on Free plan
394
+ const hasAIKey = !!(0, provider_1.getAIProvider)();
395
+ if (licenseStatus.plan !== "pro" && !hasAIKey) {
394
396
  return {
395
- content: [{ type: "text", text: getProToolError("ai_analyze") }],
397
+ content: [{ type: "text", text: getProToolError("ai_analyze") + "\n\nOr set GEMINI_KEY / OPENAI_KEY / ANTHROPIC_KEY to use ai_analyze with your own API key." }],
396
398
  };
397
399
  }
398
400
  const file = args.file;
@@ -408,10 +410,18 @@ async function main() {
408
410
  ],
409
411
  };
410
412
  }
413
+ if (!isFileInFreeSet(resolved, graph)) {
414
+ return { content: [{ type: "text", text: PRO_UPGRADE_MSG }] };
415
+ }
411
416
  const impactResult = (0, analyze_impact_1.analyzeImpact)(resolved, graph);
412
417
  const aiResult = await (0, analyzer_1.analyzeWithAI)(resolved, impactResult, graph);
418
+ // Free tier: append partial analysis warning
419
+ let resultText = aiResult;
420
+ if (licenseStatus.plan !== "pro" && graph.files.size > FREE_MAX_FILES) {
421
+ resultText += `\n\n---\n⚠️ Free tier: analyzing ${FREE_MAX_FILES}/${graph.files.size} files. Some dependencies may be missing. Upgrade to Pro for full analysis: https://syke.cloud/dashboard/`;
422
+ }
413
423
  return {
414
- content: [{ type: "text", text: appendDashboardFooter(aiResult) }],
424
+ content: [{ type: "text", text: appendDashboardFooter(resultText) }],
415
425
  };
416
426
  }
417
427
  case "check_warnings": {
@@ -475,7 +485,7 @@ async function main() {
475
485
  }
476
486
  });
477
487
  // Pre-warm the graph (skip if no project root — e.g. Smithery scan)
478
- console.error(`[syke] Starting SYKE MCP Server v1.3.4`);
488
+ console.error(`[syke] Starting SYKE MCP Server v1.3.5`);
479
489
  console.error(`[syke] License: ${licenseStatus.plan.toUpperCase()} (${licenseStatus.source})`);
480
490
  if (licenseStatus.expiresAt) {
481
491
  console.error(`[syke] Expires: ${licenseStatus.expiresAt}`);
@@ -542,7 +552,7 @@ async function main() {
542
552
  }
543
553
  // Start Express web server with file cache for SSE (only if project detected)
544
554
  if (currentProjectRoot) {
545
- const webApp = (0, server_1.createWebServer)(() => (0, graph_1.getGraph)(currentProjectRoot, currentPackageName), fileCache, switchProject, () => currentProjectRoot, () => currentPackageName, () => licenseStatus);
555
+ const webApp = (0, server_1.createWebServer)(() => (0, graph_1.getGraph)(currentProjectRoot, currentPackageName), fileCache, switchProject, () => currentProjectRoot, () => currentPackageName, () => licenseStatus, () => !!(0, provider_1.getAIProvider)());
546
556
  webApp.listen(WEB_PORT, () => {
547
557
  const dashUrl = `http://localhost:${WEB_PORT}`;
548
558
  console.error(`[syke] Web dashboard: ${dashUrl}`);
@@ -570,7 +580,7 @@ main().catch((err) => {
570
580
  * See: https://smithery.ai/docs/deploy#sandbox-server
571
581
  */
572
582
  function createSandboxServer() {
573
- const sandboxServer = new index_js_1.Server({ name: "syke", version: "1.3.4" }, { capabilities: { tools: {} } });
583
+ const sandboxServer = new index_js_1.Server({ name: "syke", version: "1.3.5" }, { capabilities: { tools: {} } });
574
584
  sandboxServer.setRequestHandler(types_js_1.ListToolsRequestSchema, async () => ({
575
585
  tools: [
576
586
  {
@@ -114,7 +114,8 @@ function discoverAllFiles(rootDir, extensions, extraSkipDirs) {
114
114
  try {
115
115
  entries = fs.readdirSync(dir, { withFileTypes: true });
116
116
  }
117
- catch {
117
+ catch (err) {
118
+ console.error(`[syke] discoverAllFiles walk error for ${dir}: ${err.message}`);
118
119
  return;
119
120
  }
120
121
  for (const entry of entries) {
@@ -178,7 +179,9 @@ function findSourceDirsWithFiles(root, extensions) {
178
179
  }
179
180
  }
180
181
  }
181
- catch { }
182
+ catch (err) {
183
+ console.error(`[syke] findSourceDirsWithFiles error for ${root}: ${err.message}`);
184
+ }
182
185
  return dirs;
183
186
  }
184
187
  // ── Register All Plugins ──
@@ -42,9 +42,9 @@ const path = __importStar(require("path"));
42
42
  const os = __importStar(require("os"));
43
43
  const https = __importStar(require("https"));
44
44
  const crypto = __importStar(require("crypto"));
45
- const CACHE_DIR = path.join(os.homedir(), ".syke");
45
+ const config_1 = require("../config");
46
+ const CACHE_DIR = config_1.CONFIG_DIR_PATH;
46
47
  const CACHE_FILE = path.join(CACHE_DIR, ".license-cache.json");
47
- const CONFIG_FILE = path.join(CACHE_DIR, "config.json");
48
48
  // Cache durations
49
49
  const CACHE_MAX_AGE = 24 * 60 * 60 * 1000; // 24 hours
50
50
  const CACHE_GRACE_PERIOD = 7 * 24 * 60 * 60 * 1000; // 7 days offline grace
@@ -82,21 +82,9 @@ function getDeviceName() {
82
82
  * Read license key from env var or config file
83
83
  */
84
84
  function getLicenseKey() {
85
- // 1. Environment variable
86
- const envKey = process.env.SYKE_LICENSE_KEY;
87
- if (envKey && envKey.startsWith("SYKE-"))
88
- return envKey;
89
- // 2. Config file
90
- try {
91
- if (fs.existsSync(CONFIG_FILE)) {
92
- const config = JSON.parse(fs.readFileSync(CONFIG_FILE, "utf-8"));
93
- if (config.licenseKey && config.licenseKey.startsWith("SYKE-"))
94
- return config.licenseKey;
95
- }
96
- }
97
- catch {
98
- // ignore
99
- }
85
+ const key = (0, config_1.getConfig)("licenseKey", "SYKE_LICENSE_KEY");
86
+ if (key && key.startsWith("SYKE-"))
87
+ return key;
100
88
  return null;
101
89
  }
102
90
  /**
@@ -49,8 +49,25 @@ document.addEventListener("DOMContentLoaded", async () => {
49
49
  setupTabs();
50
50
  setupProjectModal();
51
51
  initSSE();
52
+ startHealthCheck();
52
53
  });
53
54
 
55
+ // Periodic server health check (catches server down even without SSE)
56
+ let healthCheckTimer = null;
57
+ function startHealthCheck() {
58
+ if (healthCheckTimer) return;
59
+ healthCheckTimer = setInterval(async () => {
60
+ try {
61
+ const res = await fetch("/api/project-info", { signal: AbortSignal.timeout(3000) });
62
+ if (res.ok) {
63
+ // Server is alive — if overlay was showing, hideServerOffline handles reconnection
64
+ }
65
+ } catch (e) {
66
+ showServerOffline();
67
+ }
68
+ }, 10000); // Check every 10 seconds
69
+ }
70
+
54
71
  // ═══════════════════════════════════════════
55
72
  // WELCOME OVERLAY
56
73
  // ═══════════════════════════════════════════
@@ -2116,12 +2133,22 @@ async function initSSE() {
2116
2133
  updateSSEStatus("PROJECT LOADED", "connected");
2117
2134
  });
2118
2135
 
2119
- sseSource.onerror = () => {
2136
+ sseSource.onerror = async () => {
2120
2137
  console.warn("[SYKE:SSE] Connection error");
2121
2138
  updateSSEStatus("OFFLINE", "offline");
2122
2139
  sseSource.close();
2123
2140
  sseSource = null;
2124
2141
  if (sseBlocked) return; // Don't reconnect if Pro-only block
2142
+
2143
+ // Check if server is actually down (not just SSE hiccup)
2144
+ try {
2145
+ const probe = await fetch("/api/project-info");
2146
+ if (!probe.ok) throw new Error("not ok");
2147
+ } catch (e) {
2148
+ // Server is truly down — show offline overlay
2149
+ showServerOffline();
2150
+ }
2151
+
2125
2152
  if (sseReconnectTimer) clearTimeout(sseReconnectTimer);
2126
2153
  sseReconnectTimer = setTimeout(initSSE, 3000);
2127
2154
  };
@@ -2262,6 +2289,124 @@ function setupResizeHandle() {
2262
2289
  // ═══════════════════════════════════════════
2263
2290
  // PROJECT SELECTOR
2264
2291
  // ═══════════════════════════════════════════
2292
+ let licenseTimerInterval = null;
2293
+
2294
+ function updateLicenseBadge(plan, expiresAt) {
2295
+ const badge = document.getElementById("license-badge");
2296
+ if (!badge) return;
2297
+
2298
+ const planEl = badge.querySelector(".license-plan");
2299
+ const timerEl = badge.querySelector(".license-timer");
2300
+
2301
+ // Clear previous timer
2302
+ if (licenseTimerInterval) {
2303
+ clearInterval(licenseTimerInterval);
2304
+ licenseTimerInterval = null;
2305
+ }
2306
+
2307
+ badge.className = "license-badge";
2308
+
2309
+ if (plan === "pro" && expiresAt) {
2310
+ const expiry = new Date(expiresAt);
2311
+ const now = new Date();
2312
+ const diffMs = expiry - now;
2313
+
2314
+ if (diffMs <= 0) {
2315
+ // Expired
2316
+ badge.classList.add("expired");
2317
+ planEl.textContent = "EXPIRED";
2318
+ timerEl.textContent = "";
2319
+ } else {
2320
+ const totalDays = Math.ceil(diffMs / (1000 * 60 * 60 * 24));
2321
+
2322
+ if (totalDays <= 3) {
2323
+ // Trial or near-expiry
2324
+ badge.classList.add("trial-urgent");
2325
+ planEl.textContent = "TRIAL";
2326
+ } else if (totalDays <= 14) {
2327
+ badge.classList.add("trial");
2328
+ planEl.textContent = "TRIAL";
2329
+ } else {
2330
+ badge.classList.add("pro");
2331
+ planEl.textContent = "PRO";
2332
+ }
2333
+
2334
+ // Live countdown
2335
+ function tick() {
2336
+ const remaining = new Date(expiresAt) - new Date();
2337
+ if (remaining <= 0) {
2338
+ timerEl.textContent = "EXPIRED";
2339
+ badge.className = "license-badge expired";
2340
+ planEl.textContent = "EXPIRED";
2341
+ clearInterval(licenseTimerInterval);
2342
+ return;
2343
+ }
2344
+ const d = Math.floor(remaining / (1000 * 60 * 60 * 24));
2345
+ const h = Math.floor((remaining % (1000 * 60 * 60 * 24)) / (1000 * 60 * 60));
2346
+ const m = Math.floor((remaining % (1000 * 60 * 60)) / (1000 * 60));
2347
+ const s = Math.floor((remaining % (1000 * 60)) / 1000);
2348
+
2349
+ if (d > 0) {
2350
+ timerEl.textContent = `D-${d} ${String(h).padStart(2,"0")}:${String(m).padStart(2,"0")}:${String(s).padStart(2,"0")}`;
2351
+ } else {
2352
+ timerEl.textContent = `${String(h).padStart(2,"0")}:${String(m).padStart(2,"0")}:${String(s).padStart(2,"0")}`;
2353
+ }
2354
+ }
2355
+ tick();
2356
+ licenseTimerInterval = setInterval(tick, 1000);
2357
+ }
2358
+ } else if (plan === "pro") {
2359
+ // Pro without expiry (permanent or subscription)
2360
+ badge.classList.add("pro");
2361
+ planEl.textContent = "PRO";
2362
+ timerEl.textContent = "";
2363
+ } else {
2364
+ // Free
2365
+ badge.classList.add("free");
2366
+ planEl.textContent = "FREE";
2367
+ timerEl.textContent = "";
2368
+ }
2369
+ }
2370
+
2371
+ let offlineRetryTimer = null;
2372
+
2373
+ function showServerOffline() {
2374
+ const overlay = document.getElementById("server-offline-overlay");
2375
+ if (overlay) overlay.classList.remove("hidden");
2376
+ // Hide other content
2377
+ const topbar = document.getElementById("topbar");
2378
+ if (topbar) topbar.style.opacity = "0.2";
2379
+
2380
+ // Auto-retry every 3 seconds
2381
+ if (!offlineRetryTimer) {
2382
+ offlineRetryTimer = setInterval(async () => {
2383
+ const retryEl = document.getElementById("offline-retry-status");
2384
+ try {
2385
+ const res = await fetch("/api/project-info");
2386
+ if (res.ok) {
2387
+ // Server is back!
2388
+ clearInterval(offlineRetryTimer);
2389
+ offlineRetryTimer = null;
2390
+ hideServerOffline();
2391
+ await loadProjectInfo();
2392
+ await loadGraph();
2393
+ await loadHubFiles();
2394
+ initSSE();
2395
+ }
2396
+ } catch (e) {
2397
+ if (retryEl) retryEl.textContent = "Retrying connection... (" + new Date().toLocaleTimeString() + ")";
2398
+ }
2399
+ }, 3000);
2400
+ }
2401
+ }
2402
+
2403
+ function hideServerOffline() {
2404
+ const overlay = document.getElementById("server-offline-overlay");
2405
+ if (overlay) overlay.classList.add("hidden");
2406
+ const topbar = document.getElementById("topbar");
2407
+ if (topbar) topbar.style.opacity = "1";
2408
+ }
2409
+
2265
2410
  async function loadProjectInfo() {
2266
2411
  try {
2267
2412
  const res = await fetch("/api/project-info");
@@ -2274,8 +2419,11 @@ async function loadProjectInfo() {
2274
2419
  el.textContent = short;
2275
2420
  el.title = info.projectRoot + " | " + info.languages.join(", ") + " | " + info.fileCount + " files";
2276
2421
  }
2422
+ hideServerOffline();
2423
+ updateLicenseBadge(info.plan, info.expiresAt);
2277
2424
  } catch (e) {
2278
2425
  console.warn("[SYKE] Failed to load project info:", e);
2426
+ showServerOffline();
2279
2427
  }
2280
2428
  }
2281
2429
 
@@ -2343,7 +2491,7 @@ async function browseDir(dirPath) {
2343
2491
  });
2344
2492
  }
2345
2493
  } catch (e) {
2346
- if (listEl) listEl.innerHTML = '<div class="browse-empty">NETWORK ERROR</div>';
2494
+ if (listEl) listEl.innerHTML = '<div class="browse-empty">CONNECTION LOST — server may have restarted</div>';
2347
2495
  }
2348
2496
  }
2349
2497
 
@@ -24,6 +24,10 @@
24
24
  <span id="current-project" class="project-path">Loading...</span>
25
25
  <button id="btn-change-project" class="top-btn" title="Switch project">OPEN</button>
26
26
  </div>
27
+ <div id="license-badge" class="license-badge free">
28
+ <span class="license-plan">FREE</span>
29
+ <span class="license-timer"></span>
30
+ </div>
27
31
  <div class="top-controls">
28
32
  <button id="btn-cycles" class="top-btn" title="Detect circular dependencies">CYCLES</button>
29
33
  <button id="btn-stats" class="top-btn" title="Toggle statistics panel">STATS</button>
@@ -57,6 +61,17 @@
57
61
  </div>
58
62
  <div id="3d-graph"></div>
59
63
  <!-- Welcome Overlay (shown when no project loaded) -->
64
+ <div id="server-offline-overlay" class="welcome-overlay hidden">
65
+ <div class="welcome-content">
66
+ <div class="welcome-logo">
67
+ <div class="welcome-pulse-ring offline-pulse"><span class="welcome-pulse-dot offline-dot"></span></div>
68
+ <span class="welcome-title">SYKE</span>
69
+ </div>
70
+ <p class="welcome-subtitle" style="color:#ff5f57">SERVER OFFLINE</p>
71
+ <p class="welcome-msg">MCP server is not running.<br>Start Claude Code to launch the server.</p>
72
+ <div id="offline-retry-status" style="font-family:var(--font-mono,'JetBrains Mono',monospace);font-size:11px;color:rgba(255,255,255,0.3);margin-top:12px">Retrying connection...</div>
73
+ </div>
74
+ </div>
60
75
  <div id="welcome-overlay" class="welcome-overlay hidden">
61
76
  <div class="welcome-content">
62
77
  <div class="welcome-logo">
@@ -123,6 +123,70 @@ body {
123
123
  text-transform: uppercase;
124
124
  }
125
125
 
126
+ /* ── License Badge ── */
127
+ .license-badge {
128
+ display: flex;
129
+ align-items: center;
130
+ gap: 8px;
131
+ padding: 5px 16px;
132
+ border-radius: 20px;
133
+ font-size: 11px;
134
+ font-weight: 700;
135
+ letter-spacing: 2px;
136
+ border: 1px solid;
137
+ transition: all 0.3s;
138
+ cursor: default;
139
+ }
140
+
141
+ .license-badge.free {
142
+ color: #8899aa;
143
+ border-color: rgba(136,153,170,0.4);
144
+ background: rgba(136,153,170,0.12);
145
+ }
146
+
147
+ .license-badge.pro {
148
+ color: #ffd700;
149
+ border-color: rgba(255,215,0,0.4);
150
+ background: rgba(255,215,0,0.08);
151
+ text-shadow: 0 0 8px rgba(255,215,0,0.3);
152
+ }
153
+
154
+ .license-badge.trial {
155
+ color: var(--risk-medium);
156
+ border-color: rgba(255,159,10,0.4);
157
+ background: rgba(255,159,10,0.08);
158
+ }
159
+
160
+ .license-badge.trial-urgent {
161
+ color: var(--risk-high);
162
+ border-color: rgba(255,45,85,0.4);
163
+ background: rgba(255,45,85,0.08);
164
+ animation: license-urgent 1.5s ease-in-out infinite;
165
+ }
166
+
167
+ .license-badge.expired {
168
+ color: var(--risk-high);
169
+ border-color: rgba(255,45,85,0.4);
170
+ background: rgba(255,45,85,0.1);
171
+ }
172
+
173
+ @keyframes license-urgent {
174
+ 0%, 100% { opacity: 1; }
175
+ 50% { opacity: 0.6; }
176
+ }
177
+
178
+ .license-plan {
179
+ font-weight: 700;
180
+ }
181
+
182
+ .license-timer {
183
+ font-size: 9px;
184
+ font-weight: 400;
185
+ letter-spacing: 1px;
186
+ opacity: 0.85;
187
+ font-variant-numeric: tabular-nums;
188
+ }
189
+
126
190
  .top-controls {
127
191
  display: flex;
128
192
  gap: 8px;
@@ -1349,8 +1413,14 @@ main {
1349
1413
  text-shadow: 0 0 8px rgba(48,209,88,0.5);
1350
1414
  }
1351
1415
  .sse-indicator.offline {
1352
- color: var(--text-secondary);
1353
- border-color: var(--text-secondary);
1416
+ color: #ff5f57;
1417
+ border-color: #ff5f57;
1418
+ text-shadow: 0 0 8px rgba(255,95,87,0.5);
1419
+ animation: offline-blink 1.5s ease-in-out infinite;
1420
+ }
1421
+ @keyframes offline-blink {
1422
+ 0%, 100% { opacity: 1; }
1423
+ 50% { opacity: 0.4; }
1354
1424
  }
1355
1425
  .sse-indicator.warning {
1356
1426
  color: var(--risk-medium);
@@ -1391,7 +1461,7 @@ main {
1391
1461
  .pulse-dot.analyzing { background: var(--accent); box-shadow: 0 0 12px var(--accent); }
1392
1462
  .pulse-dot.danger { background: var(--risk-high); box-shadow: 0 0 12px var(--risk-high); }
1393
1463
  .pulse-dot.critical { background: #ff0040; box-shadow: 0 0 16px #ff0040; }
1394
- .pulse-dot.offline { background: var(--text-secondary); box-shadow: none; }
1464
+ .pulse-dot.offline { background: #ff5f57; box-shadow: 0 0 8px rgba(255,95,87,0.5); }
1395
1465
 
1396
1466
  /* ═══════════════════════════════════════════ */
1397
1467
  /* Realtime Monitor Panel */
@@ -1888,6 +1958,9 @@ main {
1888
1958
  animation: pulse-glow 2s ease-in-out infinite;
1889
1959
  }
1890
1960
 
1961
+ .offline-pulse { border-color: #ff5f57; }
1962
+ .offline-dot { background: #ff5f57; box-shadow: 0 0 12px #ff5f57; }
1963
+
1891
1964
  .welcome-title {
1892
1965
  font-size: 42px;
1893
1966
  font-weight: 700;
@@ -29,4 +29,4 @@ export declare function createWebServer(getGraphFn: () => DependencyGraph, fileC
29
29
  expiresAt?: string;
30
30
  error?: string;
31
31
  source?: string;
32
- }): import("express-serve-static-core").Express;
32
+ }, hasAIKeyFn?: () => boolean): import("express-serve-static-core").Express;
@@ -226,7 +226,7 @@ function acknowledgeWarnings() {
226
226
  function getAllWarnings() {
227
227
  return [...warningStore];
228
228
  }
229
- function createWebServer(getGraphFn, fileCache, switchProjectFn, getProjectRoot, getPackageName, getLicenseStatus) {
229
+ function createWebServer(getGraphFn, fileCache, switchProjectFn, getProjectRoot, getPackageName, getLicenseStatus, hasAIKeyFn) {
230
230
  const app = (0, express_1.default)();
231
231
  app.use(express_1.default.json());
232
232
  // Serve static files from public/
@@ -448,12 +448,16 @@ function createWebServer(getGraphFn, fileCache, switchProjectFn, getProjectRoot,
448
448
  const result = (0, analyze_impact_1.analyzeImpact)(resolved, graph);
449
449
  res.json(result);
450
450
  });
451
- // POST /api/ai-analyze — Gemini AI semantic analysis (Pro only)
451
+ // POST /api/ai-analyze — AI semantic analysis (Pro or BYOK)
452
452
  app.post("/api/ai-analyze", async (req, res) => {
453
- // License check — Pro only
454
453
  const license = getLicenseStatus?.();
455
- if (!license || license.plan !== "pro") {
456
- return res.status(403).json(getProFeatureError("AI analysis"));
454
+ const isPro = license?.plan === "pro";
455
+ const hasKey = hasAIKeyFn?.() || false;
456
+ if (!isPro && !hasKey) {
457
+ return res.status(403).json({
458
+ ...getProFeatureError("AI analysis"),
459
+ hint: "Or set GEMINI_KEY / OPENAI_KEY / ANTHROPIC_KEY to use ai_analyze with your own API key.",
460
+ });
457
461
  }
458
462
  const { file } = req.body;
459
463
  if (!file) {
@@ -464,10 +468,19 @@ function createWebServer(getGraphFn, fileCache, switchProjectFn, getProjectRoot,
464
468
  if (!graph.files.has(resolved)) {
465
469
  return res.status(404).json({ error: `File not found in graph: ${file}` });
466
470
  }
471
+ // Free tier: check file limit
472
+ if (!isPro) {
473
+ const allFiles = [...graph.files].sort();
474
+ const idx = allFiles.indexOf(resolved);
475
+ if (idx < 0 || idx >= 50) {
476
+ return res.status(403).json({ error: "This file exceeds the Free tier limit (50 files). Upgrade to Pro for unlimited analysis.", upgrade: "https://syke.cloud/dashboard/" });
477
+ }
478
+ }
467
479
  const impactResult = (0, analyze_impact_1.analyzeImpact)(resolved, graph);
468
480
  try {
469
481
  const aiResult = await (0, analyzer_1.analyzeWithAI)(resolved, impactResult, graph);
470
- res.json({ file: impactResult.relativePath, analysis: aiResult });
482
+ const partial = !isPro && graph.files.size > 50;
483
+ res.json({ file: impactResult.relativePath, analysis: aiResult, partial });
471
484
  }
472
485
  catch (err) {
473
486
  res.status(500).json({ error: err.message || "AI analysis failed" });
@@ -717,6 +730,7 @@ function createWebServer(getGraphFn, fileCache, switchProjectFn, getProjectRoot,
717
730
  fileCount: graph.files.size,
718
731
  edgeCount,
719
732
  plan: license?.plan || "free",
733
+ expiresAt: license?.expiresAt || null,
720
734
  freeFileLimit: 50,
721
735
  });
722
736
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@syke1/mcp-server",
3
- "version": "1.3.4",
3
+ "version": "1.3.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",