@syke1/mcp-server 1.3.5 → 1.3.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.
package/README.md CHANGED
@@ -10,30 +10,39 @@ Works with **Claude Code**, **Cursor**, **Windsurf**, and any MCP-compatible AI
10
10
 
11
11
  ![SYKE Node Detail](https://syke.cloud/images/node-detail.png)
12
12
 
13
+ ## How It Works
14
+
15
+ 1. **On startup**, SYKE scans your source directory and builds a complete dependency graph using static import analysis.
16
+ 2. **Your AI agent modifies files freely** — no interruptions during normal work.
17
+ 3. **Before build/deploy**, the AI calls `gate_build` to check if all changes are safe.
18
+ 4. **If dependencies break**, SYKE detects cascading failures and blocks the build with a `FAIL` verdict.
19
+ 5. **The dashboard** shows a real-time visualization of your dependency graph with risk indicators.
20
+
21
+ > **SYKE is a safety net, not a gatekeeper.** It doesn't block your AI while working — it catches what your AI missed before you ship.
22
+
13
23
  ## Quick Start
14
24
 
15
- ### 1. Add to your MCP config
25
+ ### 1. Create config file
16
26
 
17
- **Claude Code** (`~/.claude/mcp.json`):
27
+ Create `~/.syke/config.json`:
18
28
 
19
29
  ```json
20
30
  {
21
- "mcpServers": {
22
- "syke": {
23
- "command": "npx",
24
- "args": ["@syke1/mcp-server@latest"],
25
- "env": {
26
- "SYKE_LICENSE_KEY": "your-key-here",
27
- "SYKE_WEB_PORT": "3333",
28
- "GEMINI_KEY": "your-gemini-key",
29
- "OPENAI_KEY": "your-openai-key",
30
- "ANTHROPIC_KEY": "your-anthropic-key"
31
- }
32
- }
33
- }
31
+ "licenseKey": "SYKE-XXXX-XXXX-XXXX-XXXX",
32
+ "geminiKey": "your-gemini-api-key"
34
33
  }
35
34
  ```
36
35
 
36
+ > Get your license key at [syke.cloud/dashboard](https://syke.cloud/dashboard/). You only need ONE AI key. Supported: `geminiKey`, `openaiKey`, `anthropicKey`.
37
+
38
+ ### 2. Register MCP server
39
+
40
+ **Claude Code:**
41
+
42
+ ```bash
43
+ claude mcp add syke -- npx @syke1/mcp-server@latest
44
+ ```
45
+
37
46
  **Cursor** (`.cursor/mcp.json`):
38
47
 
39
48
  ```json
@@ -41,12 +50,7 @@ Works with **Claude Code**, **Cursor**, **Windsurf**, and any MCP-compatible AI
41
50
  "mcpServers": {
42
51
  "syke": {
43
52
  "command": "npx",
44
- "args": ["@syke1/mcp-server@latest"],
45
- "env": {
46
- "SYKE_LICENSE_KEY": "your-key-here",
47
- "SYKE_WEB_PORT": "3333",
48
- "GEMINI_KEY": "your-gemini-key"
49
- }
53
+ "args": ["@syke1/mcp-server@latest"]
50
54
  }
51
55
  }
52
56
  }
@@ -59,28 +63,27 @@ Works with **Claude Code**, **Cursor**, **Windsurf**, and any MCP-compatible AI
59
63
  "mcpServers": {
60
64
  "syke": {
61
65
  "command": "npx",
62
- "args": ["@syke1/mcp-server@latest"],
63
- "env": {
64
- "SYKE_LICENSE_KEY": "your-key-here",
65
- "SYKE_WEB_PORT": "3333",
66
- "GEMINI_KEY": "your-gemini-key"
67
- }
66
+ "args": ["@syke1/mcp-server@latest"]
68
67
  }
69
68
  }
70
69
  }
71
70
  ```
72
71
 
73
- > **Note:** You only need ONE AI key. SYKE auto-selects: Gemini > OpenAI > Anthropic. Set `SYKE_AI_PROVIDER` to force a specific one.
74
-
75
72
  > **Windows note:** If `npx` is not found, use the full path: `"command": "C:\\Program Files\\nodejs\\npx.cmd"`
76
73
 
77
- ### 2. Restart your AI agent
74
+ ### 3. Add build gate to your project
78
75
 
79
- SYKE auto-detects your project language and builds the dependency graph on startup.
76
+ Add this line to your project's `CLAUDE.md` (or equivalent AI instruction file):
80
77
 
81
- ### 3. Open the dashboard
78
+ ```
79
+ After completing code changes, always run the gate_build MCP tool before committing or deploying.
80
+ ```
81
+
82
+ This ensures your AI agent automatically runs SYKE's safety check after every task — no manual prompting needed.
82
83
 
83
- A web dashboard opens automatically at `http://localhost:3333` (configurable via `SYKE_WEB_PORT`) showing your live dependency graph.
84
+ ### 4. Restart your AI agent
85
+
86
+ SYKE auto-detects your project language and builds the dependency graph on startup. Open `http://localhost:3333` to see your live dashboard.
84
87
 
85
88
  ## Features
86
89
 
@@ -101,14 +104,14 @@ A web dashboard opens automatically at `http://localhost:3333` (configurable via
101
104
 
102
105
  SYKE supports three AI providers for semantic analysis. Bring your own key:
103
106
 
104
- | Provider | Model | Env Variable |
105
- |----------|-------|-------------|
106
- | Google Gemini | `gemini-2.5-flash` | `GEMINI_KEY` |
107
- | OpenAI | `gpt-4o-mini` | `OPENAI_KEY` |
108
- | Anthropic | `claude-sonnet-4-20250514` | `ANTHROPIC_KEY` |
107
+ | Provider | Model | Config Key | Env Variable |
108
+ |----------|-------|-----------|-------------|
109
+ | Google Gemini | `gemini-2.5-flash` | `geminiKey` | `GEMINI_KEY` |
110
+ | OpenAI | `gpt-4o-mini` | `openaiKey` | `OPENAI_KEY` |
111
+ | Anthropic | `claude-sonnet-4-20250514` | `anthropicKey` | `ANTHROPIC_KEY` |
109
112
 
110
113
  **Auto-selection:** SYKE uses the first available key (Gemini > OpenAI > Anthropic).
111
- **Force provider:** Set `SYKE_AI_PROVIDER=openai` (or `gemini`, `anthropic`) to override.
114
+ **Force provider:** Set `aiProvider` in config (or `SYKE_AI_PROVIDER` env var) to override.
112
115
 
113
116
  ### Language Support
114
117
 
@@ -116,10 +119,55 @@ Auto-detected, zero-config: **Dart/Flutter**, **TypeScript/JavaScript**, **Pytho
116
119
 
117
120
  ### Web Dashboard
118
121
 
119
- Live dependency graph visualization at `localhost:3333` (or your custom `SYKE_WEB_PORT`) with:
120
- - Interactive node graph (click any file to see its connections)
122
+ Live dependency graph visualization at `localhost:3333` with:
123
+ - Interactive 3D node graph (click any file to see its connections)
121
124
  - Real-time cascade monitoring
122
125
  - Risk level indicators
126
+ - Server offline detection with auto-reconnect
127
+
128
+ ## Configuration
129
+
130
+ SYKE reads from `~/.syke/config.json` (primary) with environment variable overrides:
131
+
132
+ | Config Key | Env Variable | Description | Required |
133
+ |-----------|-------------|-------------|----------|
134
+ | `licenseKey` | `SYKE_LICENSE_KEY` | Pro license key from dashboard | No (Free tier works without) |
135
+ | `geminiKey` | `GEMINI_KEY` | Google Gemini API key for `ai_analyze` | No (any one AI key) |
136
+ | `openaiKey` | `OPENAI_KEY` | OpenAI API key for `ai_analyze` | No (any one AI key) |
137
+ | `anthropicKey` | `ANTHROPIC_KEY` | Anthropic API key for `ai_analyze` | No (any one AI key) |
138
+ | `aiProvider` | `SYKE_AI_PROVIDER` | Force AI provider: `gemini`, `openai`, or `anthropic` | No (auto-selects) |
139
+ | `port` | `SYKE_WEB_PORT` | Dashboard port (default: 3333) | No |
140
+
141
+ **Full config example** (`~/.syke/config.json`):
142
+
143
+ ```json
144
+ {
145
+ "licenseKey": "SYKE-XXXX-XXXX-XXXX-XXXX",
146
+ "geminiKey": "AIza...",
147
+ "openaiKey": "",
148
+ "anthropicKey": "",
149
+ "port": 3333
150
+ }
151
+ ```
152
+
153
+ ## Recommended Workflow
154
+
155
+ ```
156
+ You (developer) AI Agent SYKE
157
+ | | |
158
+ |-- "Add feature X" -->| |
159
+ | |-- modifies files |
160
+ | |-- modifies files |
161
+ | |-- modifies files |
162
+ | | |
163
+ | |-- gate_build --->|
164
+ | | |-- scans graph
165
+ | | |-- checks impact
166
+ | |<-- PASS/FAIL ----|
167
+ | | |
168
+ |<-- "Done. Safe to | |
169
+ | build." ----------| |
170
+ ```
123
171
 
124
172
  ## Pricing
125
173
 
@@ -136,27 +184,7 @@ Live dependency graph visualization at `localhost:3333` (or your custom `SYKE_WE
136
184
  - AI semantic analysis (BYOK — Gemini, OpenAI, or Claude)
137
185
  - Priority support
138
186
 
139
- Get your license key at [syke.cloud/dashboard](https://syke.cloud/dashboard/).
140
-
141
- ## Configuration
142
-
143
- | Environment Variable | Description | Required |
144
- |---------------------|-------------|----------|
145
- | `SYKE_LICENSE_KEY` | Pro license key from dashboard | No (Free tier works without) |
146
- | `GEMINI_KEY` | Google Gemini API key for `ai_analyze` | No (any one AI key) |
147
- | `OPENAI_KEY` | OpenAI API key for `ai_analyze` | No (any one AI key) |
148
- | `ANTHROPIC_KEY` | Anthropic API key for `ai_analyze` | No (any one AI key) |
149
- | `SYKE_AI_PROVIDER` | Force AI provider: `gemini`, `openai`, or `anthropic` | No (auto-selects) |
150
- | `SYKE_WEB_PORT` | Dashboard port (default: 3333) | No |
151
- | `SYKE_NO_BROWSER` | Set to `1` to disable auto-open browser | No |
152
- | `SYKE_currentProjectRoot` | Override auto-detected project root | No |
153
-
154
- ## How It Works
155
-
156
- 1. **On startup**, SYKE scans your source directory and builds a complete dependency graph using static import analysis.
157
- 2. **When your AI modifies a file**, call `gate_build` to check if the change is safe before building.
158
- 3. **If dependencies break**, SYKE detects cascading failures and blocks the build with a `FAIL` verdict.
159
- 4. **The dashboard** shows a real-time visualization of your dependency graph with risk indicators.
187
+ Sign up at [syke.cloud](https://syke.cloud) — 3-day free trial included.
160
188
 
161
189
  ## License
162
190
 
@@ -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.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",