@syke1/mcp-server 1.3.5 → 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"
@@ -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;
@@ -730,6 +730,7 @@ function createWebServer(getGraphFn, fileCache, switchProjectFn, getProjectRoot,
730
730
  fileCount: graph.files.size,
731
731
  edgeCount,
732
732
  plan: license?.plan || "free",
733
+ expiresAt: license?.expiresAt || null,
733
734
  freeFileLimit: 50,
734
735
  });
735
736
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@syke1/mcp-server",
3
- "version": "1.3.5",
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",