@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.
- package/dist/ai/provider.js +17 -13
- package/dist/config.d.ts +19 -0
- package/dist/config.js +98 -0
- package/dist/graph.js +2 -0
- package/dist/index.js +19 -9
- package/dist/languages/plugin.js +5 -2
- package/dist/license/validator.js +5 -17
- package/dist/web/public/app.js +150 -2
- package/dist/web/public/index.html +15 -0
- package/dist/web/public/style.css +76 -3
- package/dist/web/server.d.ts +1 -1
- package/dist/web/server.js +20 -6
- package/package.json +1 -1
package/dist/ai/provider.js
CHANGED
|
@@ -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 =
|
|
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" &&
|
|
139
|
-
cachedProvider = new GeminiProvider(
|
|
142
|
+
if (forced === "gemini" && geminiKey) {
|
|
143
|
+
cachedProvider = new GeminiProvider(geminiKey);
|
|
140
144
|
}
|
|
141
|
-
else if (forced === "openai" &&
|
|
142
|
-
cachedProvider = new OpenAIProvider(
|
|
145
|
+
else if (forced === "openai" && openaiKey) {
|
|
146
|
+
cachedProvider = new OpenAIProvider(openaiKey);
|
|
143
147
|
}
|
|
144
|
-
else if (forced === "anthropic" &&
|
|
145
|
-
cachedProvider = new AnthropicProvider(
|
|
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 (
|
|
155
|
-
cachedProvider = new GeminiProvider(
|
|
158
|
+
if (geminiKey) {
|
|
159
|
+
cachedProvider = new GeminiProvider(geminiKey);
|
|
156
160
|
}
|
|
157
|
-
else if (
|
|
158
|
-
cachedProvider = new OpenAIProvider(
|
|
161
|
+
else if (openaiKey) {
|
|
162
|
+
cachedProvider = new OpenAIProvider(openaiKey);
|
|
159
163
|
}
|
|
160
|
-
else if (
|
|
161
|
-
cachedProvider = new AnthropicProvider(
|
|
164
|
+
else if (anthropicKey) {
|
|
165
|
+
cachedProvider = new AnthropicProvider(anthropicKey);
|
|
162
166
|
}
|
|
163
167
|
else {
|
|
164
168
|
cachedProvider = null;
|
package/dist/config.d.ts
ADDED
|
@@ -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(
|
|
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.
|
|
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
|
-
//
|
|
393
|
-
|
|
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(
|
|
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.
|
|
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.
|
|
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
|
{
|
package/dist/languages/plugin.js
CHANGED
|
@@ -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
|
|
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
|
-
|
|
86
|
-
|
|
87
|
-
|
|
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
|
/**
|
package/dist/web/public/app.js
CHANGED
|
@@ -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">
|
|
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:
|
|
1353
|
-
border-color:
|
|
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:
|
|
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;
|
package/dist/web/server.d.ts
CHANGED
|
@@ -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;
|
package/dist/web/server.js
CHANGED
|
@@ -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 —
|
|
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
|
-
|
|
456
|
-
|
|
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
|
-
|
|
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.
|
|
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",
|