@syke1/mcp-server 1.8.4 → 1.8.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/README.md CHANGED
@@ -281,6 +281,12 @@ We're giving the **first 20 developers** full Pro access for **30 days** — no
281
281
 
282
282
  Spots are limited. Once they're gone, they're gone.
283
283
 
284
+ ## Source Code
285
+
286
+ This repository contains the **Free tier source code** — the core dependency graph engine, language plugins, and 3 free MCP tools (`gate_build`, `check_safe`, `get_dependencies`).
287
+
288
+ Pro and Cortex features (advanced algorithms, AI analysis, real-time monitoring, web dashboard) are included in the [npm package](https://www.npmjs.com/package/@syke1/mcp-server) as compiled code.
289
+
284
290
  ## License
285
291
 
286
292
  [Elastic License 2.0 (ELv2)](LICENSE)
package/dist/graph.d.ts CHANGED
@@ -12,5 +12,13 @@ export interface DependencyGraph {
12
12
  scc?: SCCResult;
13
13
  }
14
14
  export declare function buildGraph(projectRoot: string, packageName?: string, maxFiles?: number): DependencyGraph;
15
+ /**
16
+ * Async graph builder — reads all files in parallel batches for faster startup.
17
+ * Returns the graph + contentMap (reusable by FileCache to avoid re-reading).
18
+ */
19
+ export declare function buildGraphAsync(projectRoot: string, packageName?: string, maxFiles?: number): Promise<{
20
+ graph: DependencyGraph;
21
+ contentMap: Map<string, string>;
22
+ }>;
15
23
  export declare function getGraph(projectRoot: string, packageName?: string, maxFiles?: number): DependencyGraph;
16
24
  export declare function rebuildGraph(projectRoot: string, packageName?: string, maxFiles?: number): DependencyGraph;
package/dist/graph.js CHANGED
@@ -34,9 +34,11 @@ var __importStar = (this && this.__importStar) || (function () {
34
34
  })();
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
36
  exports.buildGraph = buildGraph;
37
+ exports.buildGraphAsync = buildGraphAsync;
37
38
  exports.getGraph = getGraph;
38
39
  exports.rebuildGraph = rebuildGraph;
39
40
  const path = __importStar(require("path"));
41
+ const fs = __importStar(require("fs/promises"));
40
42
  const plugin_1 = require("./languages/plugin");
41
43
  const typescript_1 = require("./languages/typescript");
42
44
  const scc_1 = require("./graph/scc");
@@ -109,6 +111,104 @@ function buildGraph(projectRoot, packageName, maxFiles) {
109
111
  console.error(`[syke] Graph built (${languages.join("+")}): ${files.size} files, ${countEdges(forward)} edges, ${scc.components.length} SCCs (${cyclicCount} cyclic)`);
110
112
  return graph;
111
113
  }
114
+ /**
115
+ * Async graph builder — reads all files in parallel batches for faster startup.
116
+ * Returns the graph + contentMap (reusable by FileCache to avoid re-reading).
117
+ */
118
+ async function buildGraphAsync(projectRoot, packageName, maxFiles) {
119
+ const detectedPlugins = (0, plugin_1.detectLanguages)(projectRoot);
120
+ const languages = detectedPlugins.map(p => p.id);
121
+ const forward = new Map();
122
+ const reverse = new Map();
123
+ const files = new Set();
124
+ const allSourceDirs = [];
125
+ let totalDiscovered = 0;
126
+ let fileLimitHit = false;
127
+ // Phase 1: Discover files (sync — directory walking is I/O-light)
128
+ const pluginDirFiles = new Map();
129
+ for (const plugin of detectedPlugins) {
130
+ const dirs = plugin.getSourceDirs(projectRoot);
131
+ const dirMap = new Map();
132
+ for (const dir of dirs) {
133
+ if (!allSourceDirs.includes(dir))
134
+ allSourceDirs.push(dir);
135
+ const sourceFiles = plugin.discoverFiles(dir);
136
+ totalDiscovered += sourceFiles.length;
137
+ dirMap.set(dir, sourceFiles);
138
+ for (const f of sourceFiles) {
139
+ if (maxFiles && files.size >= maxFiles) {
140
+ fileLimitHit = true;
141
+ break;
142
+ }
143
+ files.add(f);
144
+ if (!forward.has(f))
145
+ forward.set(f, []);
146
+ }
147
+ }
148
+ pluginDirFiles.set(plugin, dirMap);
149
+ }
150
+ if (fileLimitHit) {
151
+ console.error(`[syke] Free tier: loaded ${files.size}/${totalDiscovered} files (limit: ${maxFiles}). Upgrade to Pro for unlimited.`);
152
+ }
153
+ // Phase 2: Parallel file reading (biggest performance win)
154
+ const contentMap = new Map();
155
+ const filesToRead = [...files];
156
+ const BATCH_SIZE = 100;
157
+ for (let i = 0; i < filesToRead.length; i += BATCH_SIZE) {
158
+ const batch = filesToRead.slice(i, i + BATCH_SIZE);
159
+ const results = await Promise.all(batch.map(async (f) => {
160
+ try {
161
+ const content = await fs.readFile(f, "utf-8");
162
+ return [f, content];
163
+ }
164
+ catch {
165
+ return null;
166
+ }
167
+ }));
168
+ for (const r of results) {
169
+ if (r)
170
+ contentMap.set(r[0], r[1]);
171
+ }
172
+ }
173
+ // Phase 3: Parse imports using pre-read content (no I/O)
174
+ for (const [plugin, dirMap] of pluginDirFiles) {
175
+ for (const [dir, sourceFiles] of dirMap) {
176
+ for (const f of sourceFiles) {
177
+ if (!files.has(f))
178
+ continue;
179
+ const content = contentMap.get(f);
180
+ const imports = plugin.parseImports(f, projectRoot, dir, content);
181
+ const validImports = [];
182
+ for (const imp of imports) {
183
+ if (!files.has(imp))
184
+ continue;
185
+ validImports.push(imp);
186
+ const rev = reverse.get(imp) || [];
187
+ rev.push(f);
188
+ reverse.set(imp, rev);
189
+ }
190
+ forward.set(f, validImports);
191
+ }
192
+ }
193
+ }
194
+ const sourceDir = allSourceDirs[0] || path.join(projectRoot, "src");
195
+ const graph = {
196
+ forward,
197
+ reverse,
198
+ files,
199
+ projectRoot,
200
+ languages,
201
+ sourceDirs: allSourceDirs,
202
+ sourceDir,
203
+ };
204
+ (0, memo_cache_1.resetMemoCache)();
205
+ const scc = (0, scc_1.computeSCC)(graph);
206
+ graph.scc = scc;
207
+ const cyclicCount = scc.condensed.nodes.filter(n => n.isCyclic).length;
208
+ cachedGraph = graph;
209
+ console.error(`[syke] Graph built (${languages.join("+")}): ${files.size} files, ${countEdges(forward)} edges, ${scc.components.length} SCCs (${cyclicCount} cyclic)`);
210
+ return { graph, contentMap };
211
+ }
112
212
  function countEdges(forward) {
113
213
  let count = 0;
114
214
  for (const deps of forward.values()) {
package/dist/index.js CHANGED
@@ -147,7 +147,7 @@ async function main() {
147
147
  };
148
148
  process.on("SIGINT", shutdown);
149
149
  process.on("SIGTERM", shutdown);
150
- const server = new index_js_1.Server({ name: "syke", version: "1.8.4" }, { capabilities: { tools: {} } });
150
+ const server = new index_js_1.Server({ name: "syke", version: "1.8.6" }, { capabilities: { tools: {} } });
151
151
  // List tools
152
152
  server.setRequestHandler(types_js_1.ListToolsRequestSchema, async () => ({
153
153
  tools: [
@@ -628,7 +628,7 @@ async function main() {
628
628
  }
629
629
  });
630
630
  // Pre-warm the graph (skip if no project root — e.g. Smithery scan)
631
- console.error(`[syke] Starting SYKE MCP Server v1.8.4`);
631
+ console.error(`[syke] Starting SYKE MCP Server v1.8.5`);
632
632
  console.error(`[syke] License: ${licenseStatus.plan.toUpperCase()} (${licenseStatus.source})`);
633
633
  if (licenseStatus.expiresAt) {
634
634
  console.error(`[syke] Expires: ${licenseStatus.expiresAt}`);
@@ -656,9 +656,9 @@ async function main() {
656
656
  });
657
657
  const data = await res.json();
658
658
  const latest = data["dist-tags"]?.latest;
659
- if (latest && latest !== "1.8.4") {
659
+ if (latest && latest !== "1.8.6") {
660
660
  const [lM, lm, lp] = latest.split(".").map(Number);
661
- const [cM, cm, cp] = [1, 8, 1];
661
+ const [cM, cm, cp] = [1, 8, 6];
662
662
  if (lM > cM || (lM === cM && lm > cm) || (lM === cM && lm === cm && lp > cp)) {
663
663
  console.error(`[syke] Update available: v${latest}. Run: npx @syke1/mcp-server@latest`);
664
664
  }
@@ -667,17 +667,21 @@ async function main() {
667
667
  catch { }
668
668
  })();
669
669
  let fileCache = null;
670
+ let graphLoadTimeMs = 0;
670
671
  if (currentProjectRoot) {
671
672
  const detectedLangs = (0, plugin_1.detectLanguages)(currentProjectRoot).map(p => p.name).join(", ") || "none";
672
673
  console.error(`[syke] Project root: ${currentProjectRoot}`);
673
674
  console.error(`[syke] Detected languages: ${detectedLangs}`);
674
675
  console.error(`[syke] Package name: ${currentPackageName}`);
675
- const graph = (0, graph_1.getGraph)(currentProjectRoot, currentPackageName, getMaxFiles());
676
- // Initialize file cache (load ALL source files into memory)
676
+ const startTime = performance.now();
677
+ const { graph, contentMap } = await (0, graph_1.buildGraphAsync)(currentProjectRoot, currentPackageName, getMaxFiles());
678
+ // Initialize file cache from pre-read content (no re-reading files)
677
679
  fileCache = new file_cache_1.FileCache(currentProjectRoot);
678
- fileCache.initialize();
680
+ fileCache.initializeFromContentMap(contentMap);
679
681
  fileCache.setGraph(graph); // Enable incremental graph updates on file changes
680
682
  fileCache.startWatching();
683
+ graphLoadTimeMs = Math.round(performance.now() - startTime);
684
+ console.error(`[syke] Total load time: ${graphLoadTimeMs}ms`);
681
685
  }
682
686
  // Web server handle (set after server starts)
683
687
  let webServerHandle = null;
@@ -784,7 +788,9 @@ async function main() {
784
788
  activeProvider: (0, provider_1.getProviderName)(),
785
789
  forced,
786
790
  };
787
- });
791
+ },
792
+ // getGraphLoadTimeMs
793
+ () => graphLoadTimeMs);
788
794
  webServerHandle = { setFileCache: setWebFileCache };
789
795
  webApp.listen(WEB_PORT, () => {
790
796
  const dashUrl = `http://localhost:${WEB_PORT}`;
@@ -816,7 +822,7 @@ main().catch((err) => {
816
822
  * See: https://smithery.ai/docs/deploy#sandbox-server
817
823
  */
818
824
  function createSandboxServer() {
819
- const sandboxServer = new index_js_1.Server({ name: "syke", version: "1.8.4" }, { capabilities: { tools: {} } });
825
+ const sandboxServer = new index_js_1.Server({ name: "syke", version: "1.8.6" }, { capabilities: { tools: {} } });
820
826
  sandboxServer.setRequestHandler(types_js_1.ListToolsRequestSchema, async () => ({
821
827
  tools: [
822
828
  {
@@ -73,13 +73,14 @@ exports.cppPlugin = {
73
73
  discoverFiles(dir) {
74
74
  return (0, plugin_1.discoverAllFiles)(dir, [".cpp", ".cc", ".cxx", ".c", ".h", ".hpp", ".hxx"]);
75
75
  },
76
- parseImports(filePath, projectRoot, sourceDir) {
77
- let content;
78
- try {
79
- content = fs.readFileSync(filePath, "utf-8");
80
- }
81
- catch {
82
- return [];
76
+ parseImports(filePath, projectRoot, sourceDir, content) {
77
+ if (!content) {
78
+ try {
79
+ content = fs.readFileSync(filePath, "utf-8");
80
+ }
81
+ catch {
82
+ return [];
83
+ }
83
84
  }
84
85
  const fileDir = path.dirname(filePath);
85
86
  const imports = [];
@@ -38,6 +38,7 @@ const fs = __importStar(require("fs"));
38
38
  const path = __importStar(require("path"));
39
39
  const plugin_1 = require("./plugin");
40
40
  const IMPORT_RE = /^import\s+['"](.+?)['"]/;
41
+ const dartPackageNameCache = new Map();
41
42
  exports.dartPlugin = {
42
43
  id: "dart",
43
44
  name: "Dart",
@@ -63,25 +64,30 @@ exports.dartPlugin = {
63
64
  discoverFiles(dir) {
64
65
  return (0, plugin_1.discoverAllFiles)(dir, [".dart"]);
65
66
  },
66
- parseImports(filePath, projectRoot, sourceDir) {
67
- let content;
68
- try {
69
- content = fs.readFileSync(filePath, "utf-8");
70
- }
71
- catch {
72
- return [];
67
+ parseImports(filePath, projectRoot, sourceDir, content) {
68
+ if (!content) {
69
+ try {
70
+ content = fs.readFileSync(filePath, "utf-8");
71
+ }
72
+ catch {
73
+ return [];
74
+ }
73
75
  }
74
76
  const libDir = sourceDir;
75
77
  const imports = [];
76
- // Read package name for resolving package: imports
77
- let packageName = path.basename(projectRoot);
78
- try {
79
- const pubspec = fs.readFileSync(path.join(projectRoot, "pubspec.yaml"), "utf-8");
80
- const match = pubspec.match(/^name:\s*(\S+)/m);
81
- if (match)
82
- packageName = match[1];
78
+ // Read package name (cached per projectRoot)
79
+ let packageName = dartPackageNameCache.get(projectRoot);
80
+ if (!packageName) {
81
+ packageName = path.basename(projectRoot);
82
+ try {
83
+ const pubspec = fs.readFileSync(path.join(projectRoot, "pubspec.yaml"), "utf-8");
84
+ const match = pubspec.match(/^name:\s*(\S+)/m);
85
+ if (match)
86
+ packageName = match[1];
87
+ }
88
+ catch { }
89
+ dartPackageNameCache.set(projectRoot, packageName);
83
90
  }
84
- catch { }
85
91
  for (const line of content.split("\n")) {
86
92
  const trimmed = line.trim();
87
93
  if (trimmed.length > 0 &&
@@ -38,6 +38,7 @@ const fs = __importStar(require("fs"));
38
38
  const path = __importStar(require("path"));
39
39
  const plugin_1 = require("./plugin");
40
40
  const IMPORT_LINE_RE = /^\s*"([^"]+)"/;
41
+ const goModuleCache = new Map();
41
42
  exports.goPlugin = {
42
43
  id: "go",
43
44
  name: "Go",
@@ -62,23 +63,28 @@ exports.goPlugin = {
62
63
  discoverFiles(dir) {
63
64
  return (0, plugin_1.discoverAllFiles)(dir, [".go"]).filter(f => !f.endsWith("_test.go"));
64
65
  },
65
- parseImports(filePath, projectRoot, _sourceDir) {
66
- let content;
67
- try {
68
- content = fs.readFileSync(filePath, "utf-8");
69
- }
70
- catch {
71
- return [];
66
+ parseImports(filePath, projectRoot, _sourceDir, content) {
67
+ if (!content) {
68
+ try {
69
+ content = fs.readFileSync(filePath, "utf-8");
70
+ }
71
+ catch {
72
+ return [];
73
+ }
72
74
  }
73
- // Get module prefix from go.mod
74
- let modulePrefix = "";
75
- try {
76
- const goMod = fs.readFileSync(path.join(projectRoot, "go.mod"), "utf-8");
77
- const match = goMod.match(/^module\s+(\S+)/m);
78
- if (match)
79
- modulePrefix = match[1];
75
+ // Get module prefix from go.mod (cached per projectRoot)
76
+ let modulePrefix = goModuleCache.get(projectRoot);
77
+ if (modulePrefix === undefined) {
78
+ modulePrefix = "";
79
+ try {
80
+ const goMod = fs.readFileSync(path.join(projectRoot, "go.mod"), "utf-8");
81
+ const match = goMod.match(/^module\s+(\S+)/m);
82
+ if (match)
83
+ modulePrefix = match[1];
84
+ }
85
+ catch { }
86
+ goModuleCache.set(projectRoot, modulePrefix);
80
87
  }
81
- catch { }
82
88
  const imports = [];
83
89
  // Parse import block or single imports
84
90
  const importBlockMatch = content.match(/import\s*\(([\s\S]*?)\)/);
@@ -73,13 +73,14 @@ exports.javaPlugin = {
73
73
  discoverFiles(dir) {
74
74
  return (0, plugin_1.discoverAllFiles)(dir, [".java"]);
75
75
  },
76
- parseImports(filePath, _projectRoot, sourceDir) {
77
- let content;
78
- try {
79
- content = fs.readFileSync(filePath, "utf-8");
80
- }
81
- catch {
82
- return [];
76
+ parseImports(filePath, _projectRoot, sourceDir, content) {
77
+ if (!content) {
78
+ try {
79
+ content = fs.readFileSync(filePath, "utf-8");
80
+ }
81
+ catch {
82
+ return [];
83
+ }
83
84
  }
84
85
  const imports = [];
85
86
  for (const line of content.split("\n")) {
@@ -7,7 +7,7 @@ export interface LanguagePlugin {
7
7
  getSourceDirs(root: string): string[];
8
8
  getPackageName(root: string): string;
9
9
  discoverFiles(dir: string): string[];
10
- parseImports(filePath: string, projectRoot: string, sourceDir: string): string[];
10
+ parseImports(filePath: string, projectRoot: string, sourceDir: string, content?: string): string[];
11
11
  classifyLayer?(relPath: string): string | null;
12
12
  }
13
13
  export declare function registerPlugin(plugin: LanguagePlugin): void;
@@ -15,6 +15,7 @@ export declare function getPlugins(): LanguagePlugin[];
15
15
  export declare function getPluginById(id: string): LanguagePlugin | undefined;
16
16
  export declare function getPluginForFile(filePath: string): LanguagePlugin | undefined;
17
17
  export declare function detectLanguages(root: string): LanguagePlugin[];
18
+ export declare function clearDetectCache(): void;
18
19
  export declare function detectProjectRoot(startDir?: string): string;
19
20
  export declare function detectPackageName(root: string, detectedPlugins: LanguagePlugin[]): string;
20
21
  export declare function discoverAllFiles(rootDir: string, extensions: string[], extraSkipDirs?: string[]): string[];
@@ -38,6 +38,7 @@ exports.getPlugins = getPlugins;
38
38
  exports.getPluginById = getPluginById;
39
39
  exports.getPluginForFile = getPluginForFile;
40
40
  exports.detectLanguages = detectLanguages;
41
+ exports.clearDetectCache = clearDetectCache;
41
42
  exports.detectProjectRoot = detectProjectRoot;
42
43
  exports.detectPackageName = detectPackageName;
43
44
  exports.discoverAllFiles = discoverAllFiles;
@@ -60,9 +61,17 @@ function getPluginForFile(filePath) {
60
61
  const ext = path.extname(filePath).toLowerCase();
61
62
  return plugins.find(p => p.extensions.includes(ext));
62
63
  }
63
- // ── Auto-detect ──
64
+ // ── Auto-detect (cached) ──
65
+ let detectCache = null;
64
66
  function detectLanguages(root) {
65
- return plugins.filter(p => p.detectProject(root));
67
+ if (detectCache && detectCache.root === root)
68
+ return detectCache.result;
69
+ const result = plugins.filter(p => p.detectProject(root));
70
+ detectCache = { root, result };
71
+ return result;
72
+ }
73
+ function clearDetectCache() {
74
+ detectCache = null;
66
75
  }
67
76
  function detectProjectRoot(startDir) {
68
77
  let dir = startDir || process.cwd();
@@ -156,6 +165,29 @@ function hasManifestFile(root, manifests) {
156
165
  catch { }
157
166
  return false;
158
167
  }
168
+ /**
169
+ * Quick check: does a directory (recursively) contain any file with matching extensions?
170
+ * Short-circuits on the first match — much faster than collecting all files.
171
+ */
172
+ function hasAnySourceFile(dir, extensions, skipSet) {
173
+ try {
174
+ const entries = fs.readdirSync(dir, { withFileTypes: true });
175
+ for (const entry of entries) {
176
+ if (entry.isFile() && extensions.some(ext => entry.name.endsWith(ext))) {
177
+ return true;
178
+ }
179
+ }
180
+ // Check subdirectories only if no file found at this level
181
+ for (const entry of entries) {
182
+ if (entry.isDirectory() && !skipSet.has(entry.name) && !entry.name.startsWith(".")) {
183
+ if (hasAnySourceFile(path.join(dir, entry.name), extensions, skipSet))
184
+ return true;
185
+ }
186
+ }
187
+ }
188
+ catch { }
189
+ return false;
190
+ }
159
191
  /**
160
192
  * Find first-level subdirectories (and optionally root) that contain files with given extensions.
161
193
  * Skips hidden dirs and common non-source dirs (.venv, node_modules, etc.).
@@ -168,14 +200,14 @@ function findSourceDirsWithFiles(root, extensions) {
168
200
  if (entries.some(e => e.isFile() && extensions.some(ext => e.name.endsWith(ext)))) {
169
201
  dirs.push(root);
170
202
  }
171
- // Check first-level subdirectories
203
+ // Check first-level subdirectories (quick existence check, not full walk)
172
204
  for (const entry of entries) {
173
205
  if (!entry.isDirectory())
174
206
  continue;
175
207
  if (SKIP_DIRS.has(entry.name) || entry.name.startsWith("."))
176
208
  continue;
177
209
  const subdir = path.join(root, entry.name);
178
- if (discoverAllFiles(subdir, extensions).length > 0) {
210
+ if (hasAnySourceFile(subdir, extensions, SKIP_DIRS)) {
179
211
  dirs.push(subdir);
180
212
  }
181
213
  }
@@ -64,13 +64,14 @@ exports.pythonPlugin = {
64
64
  discoverFiles(dir) {
65
65
  return (0, plugin_1.discoverAllFiles)(dir, [".py"]);
66
66
  },
67
- parseImports(filePath, projectRoot, sourceDir) {
68
- let content;
69
- try {
70
- content = fs.readFileSync(filePath, "utf-8");
71
- }
72
- catch {
73
- return [];
67
+ parseImports(filePath, projectRoot, sourceDir, content) {
68
+ if (!content) {
69
+ try {
70
+ content = fs.readFileSync(filePath, "utf-8");
71
+ }
72
+ catch {
73
+ return [];
74
+ }
74
75
  }
75
76
  const imports = [];
76
77
  for (const line of content.split("\n")) {
@@ -69,13 +69,14 @@ exports.rubyPlugin = {
69
69
  discoverFiles(dir) {
70
70
  return (0, plugin_1.discoverAllFiles)(dir, [".rb"]);
71
71
  },
72
- parseImports(filePath, _projectRoot, _sourceDir) {
73
- let content;
74
- try {
75
- content = fs.readFileSync(filePath, "utf-8");
76
- }
77
- catch {
78
- return [];
72
+ parseImports(filePath, _projectRoot, _sourceDir, content) {
73
+ if (!content) {
74
+ try {
75
+ content = fs.readFileSync(filePath, "utf-8");
76
+ }
77
+ catch {
78
+ return [];
79
+ }
79
80
  }
80
81
  const fileDir = path.dirname(filePath);
81
82
  const imports = [];
@@ -64,13 +64,14 @@ exports.rustPlugin = {
64
64
  discoverFiles(dir) {
65
65
  return (0, plugin_1.discoverAllFiles)(dir, [".rs"]);
66
66
  },
67
- parseImports(filePath, _projectRoot, sourceDir) {
68
- let content;
69
- try {
70
- content = fs.readFileSync(filePath, "utf-8");
71
- }
72
- catch {
73
- return [];
67
+ parseImports(filePath, _projectRoot, sourceDir, content) {
68
+ if (!content) {
69
+ try {
70
+ content = fs.readFileSync(filePath, "utf-8");
71
+ }
72
+ catch {
73
+ return [];
74
+ }
74
75
  }
75
76
  const imports = [];
76
77
  for (const line of content.split("\n")) {
@@ -142,18 +142,26 @@ exports.typescriptPlugin = {
142
142
  discoverFiles(dir) {
143
143
  return (0, plugin_1.discoverAllFiles)(dir, [".ts", ".tsx", ".js", ".jsx"]).filter(f => !f.endsWith(".d.ts"));
144
144
  },
145
- parseImports(filePath, projectRoot, _sourceDir) {
146
- let content;
147
- try {
148
- content = fs.readFileSync(filePath, "utf-8");
149
- }
150
- catch {
151
- return [];
145
+ parseImports(filePath, projectRoot, _sourceDir, content) {
146
+ if (!content) {
147
+ try {
148
+ content = fs.readFileSync(filePath, "utf-8");
149
+ }
150
+ catch {
151
+ return [];
152
+ }
152
153
  }
153
154
  const fileDir = path.dirname(filePath);
154
155
  const imports = [];
155
156
  for (const line of content.split("\n")) {
156
157
  const trimmed = line.trim();
158
+ // Quick prefix guard — skip lines that can't be imports
159
+ if (trimmed.length === 0 || !(trimmed.charCodeAt(0) === 105 /* i */ ||
160
+ trimmed.charCodeAt(0) === 101 /* e */ ||
161
+ trimmed.charCodeAt(0) === 99 /* c */ ||
162
+ trimmed.charCodeAt(0) === 108 /* l */ ||
163
+ trimmed.charCodeAt(0) === 118 /* v */))
164
+ continue;
157
165
  let importPath = null;
158
166
  const match = trimmed.match(TS_IMPORT_RE) || trimmed.match(TS_SIDE_EFFECT_RE) || trimmed.match(JS_REQUIRE_RE);
159
167
  if (match)
@@ -44,6 +44,14 @@ export declare class FileCache extends EventEmitter {
44
44
  fileCount: number;
45
45
  totalLines: number;
46
46
  };
47
+ /**
48
+ * Initialize from a pre-read content map (avoids re-reading files after graph build).
49
+ * Used when buildGraphAsync already read all files into memory.
50
+ */
51
+ initializeFromContentMap(contentMap: Map<string, string>): {
52
+ fileCount: number;
53
+ totalLines: number;
54
+ };
47
55
  /** Start watching source directories for changes */
48
56
  startWatching(): void;
49
57
  private isWatchedFile;
@@ -106,6 +106,19 @@ class FileCache extends events_1.EventEmitter {
106
106
  console.error(`[syke:cache] Loaded ${this.cache.size} files (${totalLines.toLocaleString()} lines) into memory`);
107
107
  return { fileCount: this.cache.size, totalLines };
108
108
  }
109
+ /**
110
+ * Initialize from a pre-read content map (avoids re-reading files after graph build).
111
+ * Used when buildGraphAsync already read all files into memory.
112
+ */
113
+ initializeFromContentMap(contentMap) {
114
+ let totalLines = 0;
115
+ for (const [filePath, content] of contentMap) {
116
+ this.cache.set(path.normalize(filePath), content);
117
+ totalLines += content.split("\n").length;
118
+ }
119
+ console.error(`[syke:cache] Loaded ${this.cache.size} files (${totalLines.toLocaleString()} lines) from graph build (no re-read)`);
120
+ return { fileCount: this.cache.size, totalLines };
121
+ }
109
122
  /** Start watching source directories for changes */
110
123
  startWatching() {
111
124
  if (this.watcher)
@@ -2094,6 +2094,40 @@ function setupEventListeners() {
2094
2094
  document.getElementById("shortcuts-overlay").classList.add("hidden");
2095
2095
  });
2096
2096
 
2097
+ // Onboarding (Cortex-only)
2098
+ document.getElementById("btn-onboarding").addEventListener("click", () => {
2099
+ const modal = document.getElementById("cortex-modal");
2100
+ const planInfo = document.getElementById("cortex-plan-info");
2101
+ const upgradeLink = document.getElementById("cortex-upgrade-link");
2102
+ const plan = _currentPlan.toLowerCase();
2103
+
2104
+ if (plan === "cortex" || plan === "cortex_trial") {
2105
+ // Already on Cortex — trigger scan_project via MCP (show coming-soon notice for now)
2106
+ alert("Onboarding document generation is coming in the next update. Stay tuned!");
2107
+ return;
2108
+ }
2109
+
2110
+ // Show modal with plan-specific messaging
2111
+ if (plan === "pro" || plan === "pro_trial") {
2112
+ planInfo.textContent = "You're on Pro ($9/mo). Upgrade to Cortex for just $20/mo more.";
2113
+ upgradeLink.textContent = "UPGRADE TO CORTEX (+$20/mo)";
2114
+ } else {
2115
+ planInfo.textContent = "You're on the Free plan. Upgrade to Cortex to unlock AI-powered analysis.";
2116
+ upgradeLink.textContent = "UPGRADE TO CORTEX";
2117
+ }
2118
+ modal.classList.remove("hidden");
2119
+ });
2120
+
2121
+ document.getElementById("btn-close-cortex").addEventListener("click", () => {
2122
+ document.getElementById("cortex-modal").classList.add("hidden");
2123
+ });
2124
+
2125
+ document.getElementById("cortex-modal").addEventListener("click", (e) => {
2126
+ if (e.target.id === "cortex-modal") {
2127
+ document.getElementById("cortex-modal").classList.add("hidden");
2128
+ }
2129
+ });
2130
+
2097
2131
  // Cycles
2098
2132
  document.getElementById("btn-cycles").addEventListener("click", detectCycles);
2099
2133
  document.getElementById("btn-close-cycles").addEventListener("click", () => {
@@ -3668,10 +3702,13 @@ function hideServerOffline() {
3668
3702
  if (topbar) topbar.style.opacity = "1";
3669
3703
  }
3670
3704
 
3705
+ let _currentPlan = "free"; // track current plan for Cortex modal
3706
+
3671
3707
  async function loadProjectInfo() {
3672
3708
  try {
3673
3709
  const res = await fetch("/api/project-info");
3674
3710
  const info = await res.json();
3711
+ _currentPlan = info.plan || "free";
3675
3712
  const el = document.getElementById("current-project");
3676
3713
  if (el) {
3677
3714
  const short = info.projectRoot.length > 50
@@ -3680,6 +3717,13 @@ async function loadProjectInfo() {
3680
3717
  el.textContent = short;
3681
3718
  el.title = info.projectRoot + " | " + info.languages.join(", ") + " | " + info.fileCount + " files";
3682
3719
  }
3720
+ // Display graph load time
3721
+ const loadTimeEl = document.getElementById("stat-load-time");
3722
+ if (loadTimeEl && info.graphLoadTimeMs) {
3723
+ const ms = info.graphLoadTimeMs;
3724
+ loadTimeEl.textContent = ms >= 1000 ? (ms / 1000).toFixed(1) + "s" : ms + "ms";
3725
+ loadTimeEl.title = "Project scan + graph build time: " + ms + "ms";
3726
+ }
3683
3727
  hideServerOffline();
3684
3728
  updateLicenseBadge(info.plan, info.expiresAt);
3685
3729
  updateLicenseButton(info.plan);
@@ -35,11 +35,16 @@
35
35
  <button id="btn-change-project" class="top-btn" title="Switch project">OPEN</button>
36
36
  </div>
37
37
  <div class="top-controls">
38
+ <button id="btn-onboarding" class="top-btn cortex-btn" title="Generate onboarding document (Cortex)">ONBOARDING</button>
38
39
  <button id="btn-cycles" class="top-btn" title="Detect circular dependencies">CYCLES</button>
39
40
  <button id="btn-stats" class="top-btn" title="Toggle statistics panel">STATS</button>
40
41
  <button id="btn-shortcuts" class="top-btn" title="Keyboard shortcuts (?)">?</button>
41
42
  </div>
42
43
  <div class="stats" id="stats">
44
+ <div class="stat-block">
45
+ <span class="stat-label">LOADED</span>
46
+ <span class="stat-value" id="stat-load-time">---</span>
47
+ </div>
43
48
  <div class="stat-block">
44
49
  <span class="stat-label">NODES</span>
45
50
  <span class="stat-value" id="stat-files">---</span>
@@ -499,6 +504,21 @@
499
504
  </div>
500
505
  </div>
501
506
 
507
+ <!-- Cortex Upgrade Modal -->
508
+ <div id="cortex-modal" class="hidden">
509
+ <div class="cortex-modal-panel">
510
+ <button id="btn-close-cortex" class="cortex-close">&times;</button>
511
+ <div class="cortex-modal-icon">&#x1F9E0;</div>
512
+ <h3 class="cortex-modal-title">CORTEX REQUIRED</h3>
513
+ <p class="cortex-modal-desc">
514
+ <strong>Onboarding Document</strong> uses AI to scan your entire project and generate a comprehensive architecture guide — key files, patterns, dependencies, and team context.
515
+ </p>
516
+ <p id="cortex-plan-info" class="cortex-plan-info"></p>
517
+ <a id="cortex-upgrade-link" href="https://syke.cloud/dashboard/" target="_blank" class="cortex-upgrade-btn">UPGRADE TO CORTEX</a>
518
+ <p class="cortex-price-note">$29/mo · $249/yr · 14-day money-back guarantee</p>
519
+ </div>
520
+ </div>
521
+
502
522
  <!-- Bottom status bar -->
503
523
  <div id="bottom-bar">
504
524
  <span id="bottom-info">SYKE v--- · ---</span>
@@ -2628,6 +2628,111 @@ main {
2628
2628
  }
2629
2629
 
2630
2630
  /* ── Bottom status bar ── */
2631
+ /* ═══════════════════════════════════════════ */
2632
+ /* CORTEX BUTTON & MODAL */
2633
+ /* ═══════════════════════════════════════════ */
2634
+ .cortex-btn {
2635
+ background: linear-gradient(135deg, rgba(192, 132, 252, 0.15), rgba(0, 212, 255, 0.15)) !important;
2636
+ border: 1px solid rgba(192, 132, 252, 0.4) !important;
2637
+ color: #c084fc !important;
2638
+ position: relative;
2639
+ }
2640
+ .cortex-btn:hover {
2641
+ background: linear-gradient(135deg, rgba(192, 132, 252, 0.3), rgba(0, 212, 255, 0.3)) !important;
2642
+ border-color: rgba(192, 132, 252, 0.7) !important;
2643
+ box-shadow: 0 0 12px rgba(192, 132, 252, 0.3);
2644
+ }
2645
+
2646
+ #cortex-modal {
2647
+ position: fixed;
2648
+ inset: 0;
2649
+ z-index: 9000;
2650
+ background: rgba(0, 0, 0, 0.7);
2651
+ backdrop-filter: blur(6px);
2652
+ display: flex;
2653
+ align-items: center;
2654
+ justify-content: center;
2655
+ }
2656
+ #cortex-modal.hidden { display: none; }
2657
+
2658
+ .cortex-modal-panel {
2659
+ background: linear-gradient(160deg, #0d1b2a, #132244);
2660
+ border: 1px solid rgba(192, 132, 252, 0.3);
2661
+ border-radius: 12px;
2662
+ padding: 32px 36px;
2663
+ max-width: 420px;
2664
+ width: 90%;
2665
+ text-align: center;
2666
+ position: relative;
2667
+ box-shadow: 0 0 40px rgba(192, 132, 252, 0.15);
2668
+ }
2669
+ .cortex-close {
2670
+ position: absolute;
2671
+ top: 12px;
2672
+ right: 16px;
2673
+ background: none;
2674
+ border: none;
2675
+ color: #556677;
2676
+ font-size: 20px;
2677
+ cursor: pointer;
2678
+ }
2679
+ .cortex-close:hover { color: #fff; }
2680
+ .cortex-modal-icon {
2681
+ font-size: 36px;
2682
+ margin-bottom: 12px;
2683
+ }
2684
+ .cortex-modal-title {
2685
+ font-family: 'JetBrains Mono', monospace;
2686
+ font-size: 16px;
2687
+ color: #c084fc;
2688
+ letter-spacing: 2px;
2689
+ margin-bottom: 16px;
2690
+ }
2691
+ .cortex-modal-desc {
2692
+ font-size: 13px;
2693
+ color: #8899aa;
2694
+ line-height: 1.6;
2695
+ margin-bottom: 16px;
2696
+ }
2697
+ .cortex-modal-desc strong {
2698
+ color: #c8d6e5;
2699
+ }
2700
+ .cortex-plan-info {
2701
+ font-size: 12px;
2702
+ color: #00d4ff;
2703
+ margin-bottom: 20px;
2704
+ font-family: 'JetBrains Mono', monospace;
2705
+ }
2706
+ .cortex-upgrade-btn {
2707
+ display: inline-block;
2708
+ padding: 10px 28px;
2709
+ background: linear-gradient(135deg, #c084fc, #7c3aed);
2710
+ color: #fff;
2711
+ font-family: 'JetBrains Mono', monospace;
2712
+ font-size: 13px;
2713
+ font-weight: 600;
2714
+ letter-spacing: 1px;
2715
+ border-radius: 6px;
2716
+ text-decoration: none;
2717
+ transition: all 0.2s;
2718
+ }
2719
+ .cortex-upgrade-btn:hover {
2720
+ background: linear-gradient(135deg, #d4a5ff, #8b5cf6);
2721
+ box-shadow: 0 0 20px rgba(192, 132, 252, 0.4);
2722
+ transform: translateY(-1px);
2723
+ }
2724
+ .cortex-price-note {
2725
+ font-size: 11px;
2726
+ color: #556677;
2727
+ margin-top: 12px;
2728
+ }
2729
+
2730
+ /* Load time stat */
2731
+ #stat-load-time {
2732
+ color: #30d158;
2733
+ font-variant-numeric: tabular-nums;
2734
+ }
2735
+
2631
2736
  #bottom-bar {
2632
2737
  position: fixed;
2633
2738
  bottom: 0;
@@ -60,4 +60,4 @@ export declare function createWebServer(getGraphFn: () => DependencyGraph, initi
60
60
  success: boolean;
61
61
  activeProvider: string;
62
62
  forced: string | null;
63
- }): WebServerHandle;
63
+ }, getGraphLoadTimeMs?: () => number): WebServerHandle;
@@ -240,7 +240,7 @@ function acknowledgeWarnings() {
240
240
  function getAllWarnings() {
241
241
  return [...warningStore];
242
242
  }
243
- function createWebServer(getGraphFn, initialFileCache, switchProjectFn, getProjectRoot, getPackageName, getLicenseStatus, hasAIKeyFn, setLicenseKeyFn, setAIKeyFn, getAIInfoFn, setAIProviderFn) {
243
+ function createWebServer(getGraphFn, initialFileCache, switchProjectFn, getProjectRoot, getPackageName, getLicenseStatus, hasAIKeyFn, setLicenseKeyFn, setAIKeyFn, getAIInfoFn, setAIProviderFn, getGraphLoadTimeMs) {
244
244
  const app = (0, express_1.default)();
245
245
  app.use(express_1.default.json());
246
246
  /** Check if current license is Pro (includes pro_trial) */
@@ -842,6 +842,7 @@ function createWebServer(getGraphFn, initialFileCache, switchProjectFn, getProje
842
842
  aiProvider: aiInfo.activeProvider,
843
843
  aiKeys: aiInfo.configured,
844
844
  aiProviderForced: aiInfo.forced,
845
+ graphLoadTimeMs: getGraphLoadTimeMs ? getGraphLoadTimeMs() : 0,
845
846
  });
846
847
  });
847
848
  // POST /api/set-license-key — Set or remove license key via dashboard
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@syke1/mcp-server",
3
- "version": "1.8.4",
3
+ "version": "1.8.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",