@syke1/mcp-server 1.3.8 → 1.3.10

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/graph.js CHANGED
@@ -38,6 +38,7 @@ exports.getGraph = getGraph;
38
38
  exports.refreshGraph = refreshGraph;
39
39
  const path = __importStar(require("path"));
40
40
  const plugin_1 = require("./languages/plugin");
41
+ const typescript_1 = require("./languages/typescript");
41
42
  let cachedGraph = null;
42
43
  function buildGraph(projectRoot, packageName) {
43
44
  const detectedPlugins = (0, plugin_1.detectLanguages)(projectRoot);
@@ -103,5 +104,6 @@ function getGraph(projectRoot, packageName) {
103
104
  }
104
105
  function refreshGraph(projectRoot, packageName) {
105
106
  cachedGraph = null;
107
+ (0, typescript_1.clearAliasCache)();
106
108
  return buildGraph(projectRoot, packageName);
107
109
  }
@@ -101,6 +101,7 @@ const SKIP_DIRS = new Set([
101
101
  "node_modules", ".git", "dist", "build", ".dart_tool", ".pub-cache",
102
102
  "__pycache__", ".mypy_cache", ".pytest_cache", "venv", ".venv",
103
103
  "target", "vendor", ".gradle", "bin", "obj",
104
+ ".next", "out", ".nuxt", ".output",
104
105
  ]);
105
106
  function discoverAllFiles(rootDir, extensions, extraSkipDirs) {
106
107
  const results = [];
@@ -1,2 +1,4 @@
1
1
  import { LanguagePlugin } from "./plugin";
2
+ /** Clear cached aliases (call on project switch / graph refresh) */
3
+ export declare function clearAliasCache(): void;
2
4
  export declare const typescriptPlugin: LanguagePlugin;
@@ -34,12 +34,53 @@ var __importStar = (this && this.__importStar) || (function () {
34
34
  })();
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
36
  exports.typescriptPlugin = void 0;
37
+ exports.clearAliasCache = clearAliasCache;
37
38
  const fs = __importStar(require("fs"));
38
39
  const path = __importStar(require("path"));
39
40
  const plugin_1 = require("./plugin");
40
41
  const TS_IMPORT_RE = /(?:import|export)\s+.*?from\s+['"](.+?)['"]/;
41
42
  const TS_SIDE_EFFECT_RE = /^import\s+['"](.+?)['"]/;
42
43
  const JS_REQUIRE_RE = /(?:const|let|var)\s+\w+\s*=\s*require\s*\(\s*['"](.+?)['"]\s*\)/;
44
+ const aliasCache = new Map(); // projectRoot → aliases
45
+ function loadPathAliases(projectRoot) {
46
+ if (aliasCache.has(projectRoot))
47
+ return aliasCache.get(projectRoot);
48
+ const aliases = [];
49
+ try {
50
+ const tsconfigPath = path.join(projectRoot, "tsconfig.json");
51
+ if (!fs.existsSync(tsconfigPath)) {
52
+ aliasCache.set(projectRoot, aliases);
53
+ return aliases;
54
+ }
55
+ // Strip single-line comments (// ...) and trailing commas for lenient parsing
56
+ const raw = fs.readFileSync(tsconfigPath, "utf-8")
57
+ .replace(/\/\/.*$/gm, "")
58
+ .replace(/,\s*([\]}])/g, "$1");
59
+ const tsconfig = JSON.parse(raw);
60
+ const paths = tsconfig?.compilerOptions?.paths;
61
+ const baseUrl = tsconfig?.compilerOptions?.baseUrl || ".";
62
+ if (!paths) {
63
+ aliasCache.set(projectRoot, aliases);
64
+ return aliases;
65
+ }
66
+ const baseDir = path.resolve(projectRoot, baseUrl);
67
+ for (const [pattern, targets] of Object.entries(paths)) {
68
+ // Pattern like "@/*" → prefix "@/", target "./src/*" → baseDir + "src"
69
+ const prefix = pattern.endsWith("/*") ? pattern.slice(0, -1) : pattern;
70
+ const resolvedTargets = [];
71
+ for (const target of targets) {
72
+ const stripped = target.endsWith("/*") ? target.slice(0, -1) : target;
73
+ resolvedTargets.push(path.resolve(baseDir, stripped));
74
+ }
75
+ aliases.push({ prefix, targets: resolvedTargets });
76
+ }
77
+ }
78
+ catch (_) {
79
+ // Ignore parse errors
80
+ }
81
+ aliasCache.set(projectRoot, aliases);
82
+ return aliases;
83
+ }
43
84
  function resolveTsImport(fromDir, importPath) {
44
85
  const base = path.resolve(fromDir, importPath);
45
86
  const candidates = [
@@ -49,6 +90,7 @@ function resolveTsImport(fromDir, importPath) {
49
90
  base + ".js",
50
91
  base + ".jsx",
51
92
  path.join(base, "index.ts"),
93
+ path.join(base, "index.tsx"),
52
94
  path.join(base, "index.js"),
53
95
  ];
54
96
  for (const candidate of candidates) {
@@ -58,6 +100,24 @@ function resolveTsImport(fromDir, importPath) {
58
100
  }
59
101
  return null;
60
102
  }
103
+ /** Clear cached aliases (call on project switch / graph refresh) */
104
+ function clearAliasCache() {
105
+ aliasCache.clear();
106
+ }
107
+ function resolveAliasImport(importPath, projectRoot) {
108
+ const aliases = loadPathAliases(projectRoot);
109
+ for (const alias of aliases) {
110
+ if (importPath.startsWith(alias.prefix)) {
111
+ const rest = importPath.slice(alias.prefix.length);
112
+ for (const targetDir of alias.targets) {
113
+ const resolved = resolveTsImport(targetDir, "./" + rest);
114
+ if (resolved)
115
+ return resolved;
116
+ }
117
+ }
118
+ }
119
+ return null;
120
+ }
61
121
  exports.typescriptPlugin = {
62
122
  id: "typescript",
63
123
  name: "TypeScript",
@@ -82,7 +142,7 @@ exports.typescriptPlugin = {
82
142
  discoverFiles(dir) {
83
143
  return (0, plugin_1.discoverAllFiles)(dir, [".ts", ".tsx", ".js", ".jsx"]).filter(f => !f.endsWith(".d.ts"));
84
144
  },
85
- parseImports(filePath, _projectRoot, _sourceDir) {
145
+ parseImports(filePath, projectRoot, _sourceDir) {
86
146
  let content;
87
147
  try {
88
148
  content = fs.readFileSync(filePath, "utf-8");
@@ -100,11 +160,19 @@ exports.typescriptPlugin = {
100
160
  importPath = match[1];
101
161
  if (!importPath)
102
162
  continue;
103
- if (!importPath.startsWith("."))
104
- continue;
105
- const resolved = resolveTsImport(fileDir, importPath);
106
- if (resolved)
107
- imports.push(resolved);
163
+ // Skip bare package imports (e.g. "react", "next/link", "firebase/app")
164
+ if (importPath.startsWith(".")) {
165
+ // Relative import
166
+ const resolved = resolveTsImport(fileDir, importPath);
167
+ if (resolved)
168
+ imports.push(resolved);
169
+ }
170
+ else {
171
+ // Try path alias resolution (e.g. @/components/Sidebar → projectRoot/components/Sidebar)
172
+ const resolved = resolveAliasImport(importPath, projectRoot);
173
+ if (resolved)
174
+ imports.push(resolved);
175
+ }
108
176
  }
109
177
  return imports;
110
178
  },
@@ -27,12 +27,12 @@ const LAYER_HEX = {
27
27
  const LAYER_KEYS = ["FE", "BE", "DB", "API", "CONFIG", "UTIL"];
28
28
 
29
29
  const LAYER_CENTERS = {
30
- FE: { x: -800, y: 300, z: -300 },
31
- BE: { x: 800, y: 300, z: 300 },
32
- DB: { x: 0, y: -700, z: 600 },
33
- API: { x: 700, y: -400, z: -600 },
34
- CONFIG: { x: -700, y: 800, z: 500 },
35
- UTIL: { x: 0, y: 50, z: 0 },
30
+ FE: { x: -2500, y: 1000, z: -1000 },
31
+ BE: { x: 2500, y: 1000, z: 1000 },
32
+ DB: { x: 0, y: -2200, z: 1800 },
33
+ API: { x: 2200, y: -1200, z: -1800 },
34
+ CONFIG: { x: -2200, y: 2500, z: 1500 },
35
+ UTIL: { x: 0, y: 200, z: 0 },
36
36
  };
37
37
 
38
38
  // ═══════════════════════════════════════════
@@ -54,18 +54,23 @@ document.addEventListener("DOMContentLoaded", async () => {
54
54
 
55
55
  // Periodic server health check (catches server down even without SSE)
56
56
  let healthCheckTimer = null;
57
+ let healthFailCount = 0;
58
+ const HEALTH_FAIL_THRESHOLD = 3; // Show offline only after 3 consecutive failures
57
59
  function startHealthCheck() {
58
60
  if (healthCheckTimer) return;
59
61
  healthCheckTimer = setInterval(async () => {
60
62
  try {
61
- const res = await fetch("/api/project-info", { signal: AbortSignal.timeout(3000) });
63
+ const res = await fetch("/api/project-info", { signal: AbortSignal.timeout(10000) });
62
64
  if (res.ok) {
63
- // Server is alive if overlay was showing, hideServerOffline handles reconnection
65
+ healthFailCount = 0; // Reset on success
64
66
  }
65
67
  } catch (e) {
66
- showServerOffline();
68
+ healthFailCount++;
69
+ if (healthFailCount >= HEALTH_FAIL_THRESHOLD) {
70
+ showServerOffline();
71
+ }
67
72
  }
68
- }, 10000); // Check every 10 seconds
73
+ }, 30000); // Check every 30 seconds (not 10)
69
74
  }
70
75
 
71
76
  // ═══════════════════════════════════════════
@@ -112,9 +117,9 @@ async function loadGraph() {
112
117
  lineCount: n.data.lineCount || 0, importsCount: n.data.importsCount || 0,
113
118
  depth: n.data.depth || 0, group: n.data.group,
114
119
  layer, action: n.data.action || "X", env: n.data.env || "PROD",
115
- x: c.x + (Math.random() - 0.5) * 200,
116
- y: c.y + (Math.random() - 0.5) * 200,
117
- z: c.z + (Math.random() - 0.5) * 200,
120
+ x: c.x + (Math.random() - 0.5) * 600,
121
+ y: c.y + (Math.random() - 0.5) * 600,
122
+ z: c.z + (Math.random() - 0.5) * 600,
118
123
  };
119
124
  });
120
125
  const links = raw.edges.map(e => ({ source: e.data.source, target: e.data.target }));
@@ -140,7 +145,7 @@ async function loadGraph() {
140
145
  .nodeColor(node => getNodeColor(node))
141
146
  .nodeVal(node => {
142
147
  if (!isNodeVisible(node)) return 0.001;
143
- const base = Math.max(2, Math.sqrt(node.lineCount / 3));
148
+ const base = Math.max(8, Math.sqrt(node.lineCount) * 0.8);
144
149
  const hb = heartbeatNodes.get(node.id);
145
150
  if (hb) {
146
151
  const elapsed = Date.now() - hb.startTime;
@@ -215,19 +220,19 @@ async function loadGraph() {
215
220
  node.fz = undefined;
216
221
  })
217
222
 
218
- .d3AlphaDecay(0.015)
219
- .d3VelocityDecay(0.35)
220
- .warmupTicks(100)
221
- .cooldownTicks(500)
223
+ .d3AlphaDecay(0.008)
224
+ .d3VelocityDecay(0.3)
225
+ .warmupTicks(300)
226
+ .cooldownTicks(800)
222
227
  .enablePointerInteraction(true);
223
228
 
224
- Graph.d3Force("cluster", clusterForce(0.25));
225
- Graph.d3Force("charge").strength(-25);
229
+ Graph.d3Force("cluster", clusterForce(0.015));
230
+ Graph.d3Force("charge").strength(-800);
226
231
  Graph.d3Force("link")
227
- .distance(l => srcLayer(l) === tgtLayer(l) ? 35 : 150)
228
- .strength(l => srcLayer(l) === tgtLayer(l) ? 0.8 : 0.2);
232
+ .distance(l => srcLayer(l) === tgtLayer(l) ? 250 : 900)
233
+ .strength(l => srcLayer(l) === tgtLayer(l) ? 0.2 : 0.05);
229
234
 
230
- Graph.cameraPosition({ x: 0, y: 0, z: 1600 });
235
+ Graph.cameraPosition({ x: 0, y: 0, z: 3500 });
231
236
 
232
237
  setTimeout(() => {
233
238
  try {
@@ -257,6 +262,12 @@ async function loadGraph() {
257
262
  buildLegend(layerCounts);
258
263
  createNodeLabels();
259
264
  updateLabelsLoop();
265
+
266
+ // ── Auto-start code crawl with highest-hub file on initial load ──
267
+ setTimeout(() => {
268
+ const topNode = nodes.slice().sort((a, b) => b.dependentCount - a.dependentCount)[0];
269
+ if (topNode) startCodeCrawl(topNode.id);
270
+ }, 2000);
260
271
  }
261
272
 
262
273
  // ═══════════════════════════════════════════
@@ -604,7 +615,7 @@ function handleBackgroundClick() {
604
615
  document.getElementById("code-content").innerHTML = '<p class="placeholder">Select a node to preview source code</p>';
605
616
  document.getElementById("sim-content").innerHTML = '<p class="placeholder">Select a node, then switch to SIM tab</p>';
606
617
  document.getElementById("btn-ai-analyze").disabled = true;
607
- stopCodeCrawl();
618
+ // Don't stop code crawl — keep it always visible
608
619
  refreshGraph();
609
620
  }
610
621
 
@@ -1578,10 +1589,52 @@ function stopCodeCrawl() {
1578
1589
  if (crawlUserTimer) { clearTimeout(crawlUserTimer); crawlUserTimer = null; }
1579
1590
  const vp = document.getElementById("crawl-viewport");
1580
1591
  if (vp) { vp.removeEventListener("wheel", pauseAutoScroll); vp.removeEventListener("mousedown", pauseAutoScroll); vp.removeEventListener("touchstart", pauseAutoScroll); }
1581
- document.getElementById("code-crawl").classList.remove("active");
1592
+ // Code crawl stays always visible — never hide
1582
1593
  crawlData = null;
1583
1594
  }
1584
1595
 
1596
+ // ═══════════════════════════════════════════
1597
+ // CODE CRAWL: DRAG SUPPORT
1598
+ // ═══════════════════════════════════════════
1599
+ (function initCrawlDrag() {
1600
+ let isDragging = false, startX, startY, startLeft, startTop;
1601
+ document.addEventListener("DOMContentLoaded", () => {
1602
+ const crawlEl = document.getElementById("code-crawl");
1603
+ const header = document.getElementById("crawl-header");
1604
+ if (!crawlEl || !header) return;
1605
+
1606
+ header.addEventListener("mousedown", (e) => {
1607
+ if (e.target.closest("#crawl-viewport")) return;
1608
+ isDragging = true;
1609
+ crawlEl.classList.add("dragging");
1610
+ const rect = crawlEl.getBoundingClientRect();
1611
+ const parent = crawlEl.offsetParent.getBoundingClientRect();
1612
+ startX = e.clientX;
1613
+ startY = e.clientY;
1614
+ startLeft = rect.left - parent.left;
1615
+ startTop = rect.top - parent.top;
1616
+ crawlEl.style.left = startLeft + "px";
1617
+ crawlEl.style.top = startTop + "px";
1618
+ crawlEl.style.bottom = "auto";
1619
+ e.preventDefault();
1620
+ });
1621
+
1622
+ document.addEventListener("mousemove", (e) => {
1623
+ if (!isDragging) return;
1624
+ const dx = e.clientX - startX;
1625
+ const dy = e.clientY - startY;
1626
+ crawlEl.style.left = (startLeft + dx) + "px";
1627
+ crawlEl.style.top = (startTop + dy) + "px";
1628
+ });
1629
+
1630
+ document.addEventListener("mouseup", () => {
1631
+ if (!isDragging) return;
1632
+ isDragging = false;
1633
+ crawlEl.classList.remove("dragging");
1634
+ });
1635
+ });
1636
+ })();
1637
+
1585
1638
  // ═══════════════════════════════════════════
1586
1639
  // REAL-TIME DIFF VIEWER (SSE-driven)
1587
1640
  // ═══════════════════════════════════════════
@@ -1683,6 +1736,9 @@ function showRealtimeDiff(data) {
1683
1736
 
1684
1737
  contentEl.innerHTML = html;
1685
1738
 
1739
+ // ── Mirror diff to CODE tab (VS Code style) ──
1740
+ renderCodeTabDiff(data, html);
1741
+
1686
1742
  // ── Animated diff scroll: sweep through file, pause on changes ──
1687
1743
  if (diffScrollAnim) cancelAnimationFrame(diffScrollAnim);
1688
1744
  viewport.scrollTop = 0;
@@ -1768,6 +1824,97 @@ function updateDiffWithAnalysis(analysis) {
1768
1824
  }
1769
1825
 
1770
1826
  // ═══════════════════════════════════════════
1827
+ // CODE TAB: VS Code-style real-time diff viewer
1828
+ // ═══════════════════════════════════════════
1829
+ let codeTabScrollAnim = null;
1830
+
1831
+ function renderCodeTabDiff(data, diffHtml) {
1832
+ const codeEl = document.getElementById("code-content");
1833
+ if (!codeEl) return;
1834
+
1835
+ // Build VS Code-style header
1836
+ const fileName = data.file.split("/").pop();
1837
+ const ext = fileName.split(".").pop();
1838
+ const langLabel = { ts: "TypeScript", tsx: "TypeScript React", js: "JavaScript", jsx: "JSX",
1839
+ dart: "Dart", py: "Python", go: "Go", rs: "Rust", java: "Java", cpp: "C++", rb: "Ruby",
1840
+ css: "CSS", html: "HTML", json: "JSON", md: "Markdown", yaml: "YAML", yml: "YAML" }[ext] || ext.toUpperCase();
1841
+ const changeIcon = data.type === "added" ? "A" : data.type === "deleted" ? "D" : "M";
1842
+ const changeCls = data.type === "added" ? "vsc-added" : data.type === "deleted" ? "vsc-deleted" : "vsc-modified";
1843
+
1844
+ let html = `<div class="vsc-editor">`;
1845
+ // Tab bar (like VS Code file tab)
1846
+ html += `<div class="vsc-tab-bar">`;
1847
+ html += `<div class="vsc-tab active">`;
1848
+ html += `<span class="vsc-tab-change ${changeCls}">${changeIcon}</span>`;
1849
+ html += `<span class="vsc-tab-name">${fileName}</span>`;
1850
+ html += `<span class="vsc-tab-lang">${langLabel}</span>`;
1851
+ html += `</div>`;
1852
+ html += `</div>`;
1853
+ // Breadcrumb path
1854
+ html += `<div class="vsc-breadcrumb">${data.file}</div>`;
1855
+ // Diff content area (reuse crawl-line classes)
1856
+ html += `<div class="vsc-diff-body">${diffHtml}</div>`;
1857
+ // Status bar
1858
+ const lineCount = data.newContent ? data.newContent.length : 0;
1859
+ const diffCount = data.diffCount || 0;
1860
+ html += `<div class="vsc-status-bar">`;
1861
+ html += `<span class="vsc-status-item">${langLabel}</span>`;
1862
+ html += `<span class="vsc-status-item">Ln ${lineCount}</span>`;
1863
+ html += `<span class="vsc-status-item vsc-status-diff">+${diffCount} changes</span>`;
1864
+ html += `<span class="vsc-status-item">${new Date().toLocaleTimeString()}</span>`;
1865
+ html += `</div>`;
1866
+ html += `</div>`;
1867
+
1868
+ codeEl.innerHTML = html;
1869
+
1870
+ // Auto-switch to CODE tab
1871
+ switchTab("code");
1872
+
1873
+ // Auto-scroll to first change
1874
+ if (codeTabScrollAnim) cancelAnimationFrame(codeTabScrollAnim);
1875
+ setTimeout(() => {
1876
+ const diffBody = codeEl.querySelector(".vsc-diff-body");
1877
+ const firstChange = diffBody?.querySelector(".crawl-line.added, .crawl-line.changed, .crawl-line.removed");
1878
+ if (firstChange && diffBody) {
1879
+ firstChange.scrollIntoView({ behavior: "smooth", block: "center" });
1880
+ firstChange.classList.add("diff-flash-active");
1881
+ setTimeout(() => firstChange.classList.remove("diff-flash-active"), 1200);
1882
+ }
1883
+ }, 300);
1884
+ }
1885
+
1886
+ function updateCodeTabAnalysis(analysis) {
1887
+ const statusBar = document.querySelector(".vsc-status-bar");
1888
+ if (!statusBar) return;
1889
+
1890
+ const riskColors = { CRITICAL: "#f85149", HIGH: "#ff6b6b", MEDIUM: "#e3b341", LOW: "#3fb950", SAFE: "#3fb950" };
1891
+ const color = riskColors[analysis.riskLevel] || "#888";
1892
+ statusBar.style.background = analysis.riskLevel === "CRITICAL" || analysis.riskLevel === "HIGH" ? "#6e1b1b" : "#007acc";
1893
+
1894
+ // Add risk badge to status bar
1895
+ const existing = statusBar.querySelector(".vsc-risk-badge");
1896
+ if (existing) existing.remove();
1897
+ const badge = document.createElement("span");
1898
+ badge.className = "vsc-risk-badge";
1899
+ badge.style.cssText = `background:${color};color:#fff;padding:1px 6px;border-radius:2px;font-weight:bold;font-size:10px;`;
1900
+ badge.textContent = analysis.riskLevel;
1901
+ statusBar.prepend(badge);
1902
+
1903
+ // Add AI summary banner to diff body
1904
+ const diffBody = document.querySelector(".vsc-diff-body");
1905
+ if (diffBody && analysis.summary) {
1906
+ const banner = document.createElement("div");
1907
+ banner.className = "vsc-ai-banner";
1908
+ banner.style.cssText = `padding:8px 16px;background:rgba(0,122,204,0.15);border-left:3px solid ${color};color:${color};font-size:11px;margin:0;`;
1909
+ let text = `AI: ${analysis.summary}`;
1910
+ if (analysis.suggestion) text += ` — ${analysis.suggestion}`;
1911
+ banner.textContent = text;
1912
+ const firstBanner = diffBody.querySelector(".diff-banner");
1913
+ if (firstBanner) firstBanner.after(banner);
1914
+ else diffBody.prepend(banner);
1915
+ }
1916
+ }
1917
+
1771
1918
  // ═══════════════════════════════════════════
1772
1919
  // AUDIT RESULT (shown after analysis completes)
1773
1920
  // ═══════════════════════════════════════════
@@ -1798,14 +1945,7 @@ function showAuditResult(analysis) {
1798
1945
  <div class="audit-meta">${analysis.affectedNodes?.length || 0} files checked · ${analysis.analysisMs}ms</div>
1799
1946
  </div>`;
1800
1947
 
1801
- // Fade out after 8s
1802
- setTimeout(() => {
1803
- crawlEl.classList.remove("active");
1804
- contentEl.innerHTML = "";
1805
- headerName.textContent = "";
1806
- headerStatus.textContent = "";
1807
- headerStatus.style.color = "";
1808
- }, 8000);
1948
+ // Keep showing audit result — don't hide code crawl
1809
1949
  } else {
1810
1950
  // ── WARNING: issues found — keep showing ──
1811
1951
  headerName.textContent = analysis.file;
@@ -1962,6 +2102,7 @@ async function initSSE() {
1962
2102
  sseSource.addEventListener("connected", (e) => {
1963
2103
  const data = JSON.parse(e.data);
1964
2104
  console.log("[SYKE:SSE] Connected, cache:", data.cacheSize, "files");
2105
+ healthFailCount = 0; // Reset health failures on SSE connect
1965
2106
  updateSSEStatus("LIVE", "connected");
1966
2107
  });
1967
2108
 
@@ -2055,6 +2196,8 @@ async function initSSE() {
2055
2196
 
2056
2197
  // Update diff view with analysis risk badge
2057
2198
  updateDiffWithAnalysis(analysis);
2199
+ // Also update CODE tab with analysis result
2200
+ updateCodeTabAnalysis(analysis);
2058
2201
 
2059
2202
  addRealtimeEvent({
2060
2203
  type: "result",
@@ -2133,24 +2276,34 @@ async function initSSE() {
2133
2276
  updateSSEStatus("PROJECT LOADED", "connected");
2134
2277
  });
2135
2278
 
2279
+ let sseRetryCount = 0;
2136
2280
  sseSource.onerror = async () => {
2137
- console.warn("[SYKE:SSE] Connection error");
2138
- updateSSEStatus("OFFLINE", "offline");
2281
+ console.warn("[SYKE:SSE] Connection error, retry #" + (sseRetryCount + 1));
2139
2282
  sseSource.close();
2140
2283
  sseSource = null;
2141
- if (sseBlocked) return; // Don't reconnect if Pro-only block
2284
+ if (sseBlocked) return;
2142
2285
 
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();
2286
+ sseRetryCount++;
2287
+ updateSSEStatus("RECONNECTING...", "warning");
2288
+
2289
+ // Only show offline after 5 consecutive SSE failures
2290
+ if (sseRetryCount >= 5) {
2291
+ try {
2292
+ const probe = await fetch("/api/project-info", { signal: AbortSignal.timeout(8000) });
2293
+ if (!probe.ok) throw new Error("not ok");
2294
+ // Server alive but SSE failing — just keep retrying
2295
+ sseRetryCount = 0;
2296
+ } catch (e) {
2297
+ showServerOffline();
2298
+ }
2150
2299
  }
2151
2300
 
2301
+ // Exponential backoff: 2s, 4s, 8s, 16s, max 30s
2302
+ const delay = Math.min(2000 * Math.pow(2, sseRetryCount - 1), 30000);
2152
2303
  if (sseReconnectTimer) clearTimeout(sseReconnectTimer);
2153
- sseReconnectTimer = setTimeout(initSSE, 3000);
2304
+ sseReconnectTimer = setTimeout(() => {
2305
+ initSSE();
2306
+ }, delay);
2154
2307
  };
2155
2308
  }
2156
2309
 
@@ -2423,7 +2576,7 @@ async function loadProjectInfo() {
2423
2576
  updateLicenseBadge(info.plan, info.expiresAt);
2424
2577
  } catch (e) {
2425
2578
  console.warn("[SYKE] Failed to load project info:", e);
2426
- showServerOffline();
2579
+ // Don't immediately show offline — let health check handle it
2427
2580
  }
2428
2581
  }
2429
2582
 
@@ -660,9 +660,11 @@ main {
660
660
 
661
661
  #code-content {
662
662
  flex: 1;
663
- overflow-y: auto;
663
+ overflow: hidden;
664
664
  font-size: 11px;
665
665
  line-height: 1.6;
666
+ display: flex;
667
+ flex-direction: column;
666
668
  }
667
669
 
668
670
  .code-block {
@@ -689,6 +691,148 @@ main {
689
691
  opacity: 0.5;
690
692
  }
691
693
 
694
+ /* ── VS Code-style Code Editor (CODE tab) ── */
695
+ .vsc-editor {
696
+ display: flex;
697
+ flex-direction: column;
698
+ height: 100%;
699
+ background: #1e1e1e;
700
+ border: 1px solid #333;
701
+ border-radius: 4px;
702
+ overflow: hidden;
703
+ }
704
+
705
+ .vsc-tab-bar {
706
+ display: flex;
707
+ align-items: center;
708
+ background: #252526;
709
+ border-bottom: 1px solid #333;
710
+ min-height: 36px;
711
+ padding: 0 4px;
712
+ }
713
+
714
+ .vsc-tab {
715
+ display: flex;
716
+ align-items: center;
717
+ gap: 6px;
718
+ padding: 6px 12px;
719
+ background: #1e1e1e;
720
+ border-top: 2px solid #007acc;
721
+ border-right: 1px solid #333;
722
+ font-size: 12px;
723
+ color: #ccc;
724
+ white-space: nowrap;
725
+ }
726
+
727
+ .vsc-tab-change {
728
+ font-size: 10px;
729
+ font-weight: bold;
730
+ padding: 1px 4px;
731
+ border-radius: 2px;
732
+ }
733
+ .vsc-tab-change.vsc-modified { color: #e2c08d; }
734
+ .vsc-tab-change.vsc-added { color: #73c991; }
735
+ .vsc-tab-change.vsc-deleted { color: #f44747; }
736
+
737
+ .vsc-tab-name { color: #fff; }
738
+ .vsc-tab-lang { color: #666; font-size: 10px; }
739
+
740
+ .vsc-breadcrumb {
741
+ padding: 4px 12px;
742
+ font-size: 11px;
743
+ color: #888;
744
+ background: #1e1e1e;
745
+ border-bottom: 1px solid #2a2a2a;
746
+ white-space: nowrap;
747
+ overflow: hidden;
748
+ text-overflow: ellipsis;
749
+ }
750
+
751
+ .vsc-diff-body {
752
+ flex: 1;
753
+ overflow-y: auto;
754
+ overflow-x: auto;
755
+ background: #1e1e1e;
756
+ padding: 4px 0;
757
+ font-family: 'Consolas', 'SF Mono', 'Fira Code', monospace;
758
+ font-size: 12px;
759
+ line-height: 20px;
760
+ }
761
+
762
+ /* Scrollbar styling for diff body */
763
+ .vsc-diff-body::-webkit-scrollbar { width: 10px; }
764
+ .vsc-diff-body::-webkit-scrollbar-track { background: #1e1e1e; }
765
+ .vsc-diff-body::-webkit-scrollbar-thumb { background: #424242; border-radius: 4px; }
766
+ .vsc-diff-body::-webkit-scrollbar-thumb:hover { background: #555; }
767
+
768
+ /* Override crawl-line in diff body for VS Code look */
769
+ .vsc-diff-body .crawl-line {
770
+ padding: 0 16px 0 0;
771
+ min-height: 20px;
772
+ display: flex;
773
+ align-items: center;
774
+ border: none;
775
+ margin: 0;
776
+ font-size: 12px;
777
+ }
778
+
779
+ .vsc-diff-body .crawl-line .cl-num {
780
+ min-width: 48px;
781
+ padding-right: 12px;
782
+ text-align: right;
783
+ color: #5a5a5a;
784
+ user-select: none;
785
+ font-size: 12px;
786
+ background: #1e1e1e;
787
+ }
788
+
789
+ .vsc-diff-body .crawl-line.added {
790
+ background: rgba(35, 134, 54, 0.2);
791
+ border-left: 3px solid #28a745;
792
+ }
793
+ .vsc-diff-body .crawl-line.added .cl-num { background: rgba(35, 134, 54, 0.15); color: #73c991; }
794
+
795
+ .vsc-diff-body .crawl-line.removed {
796
+ background: rgba(248, 81, 73, 0.15);
797
+ border-left: 3px solid #f85149;
798
+ }
799
+ .vsc-diff-body .crawl-line.removed .cl-num { background: rgba(248, 81, 73, 0.1); color: #f85149; }
800
+
801
+ .vsc-diff-body .crawl-line.changed {
802
+ background: rgba(227, 179, 65, 0.12);
803
+ border-left: 3px solid #e3b341;
804
+ }
805
+ .vsc-diff-body .crawl-line.changed .cl-num { background: rgba(227, 179, 65, 0.08); color: #e3b341; }
806
+
807
+ .vsc-diff-body .diff-banner {
808
+ padding: 6px 16px;
809
+ font-size: 11px;
810
+ margin: 0;
811
+ border-radius: 0;
812
+ border-left: 3px solid;
813
+ }
814
+
815
+ .vsc-diff-body .diff-marker {
816
+ min-width: 16px;
817
+ text-align: center;
818
+ font-weight: bold;
819
+ }
820
+
821
+ .vsc-status-bar {
822
+ display: flex;
823
+ align-items: center;
824
+ gap: 16px;
825
+ padding: 2px 12px;
826
+ background: #007acc;
827
+ color: #fff;
828
+ font-size: 11px;
829
+ min-height: 24px;
830
+ flex-shrink: 0;
831
+ }
832
+
833
+ .vsc-status-item { white-space: nowrap; }
834
+ .vsc-status-diff { font-weight: bold; }
835
+
692
836
  /* ── Simulation Panel ── */
693
837
  #sim-panel {
694
838
  flex: 1;
@@ -1192,52 +1336,134 @@ main {
1192
1336
  STAR WARS CODE CRAWL
1193
1337
  ═══════════════════════════════════════════ */
1194
1338
 
1339
+ /* ── FBI/CIA Code Crawl Overlay ── */
1195
1340
  #code-crawl {
1196
1341
  position: absolute;
1197
- right: 20px;
1198
- top: 60px;
1342
+ left: 20px;
1343
+ bottom: 20px;
1199
1344
  width: 500px;
1200
- max-height: 500px;
1345
+ height: 500px;
1201
1346
  pointer-events: auto;
1202
- z-index: 7;
1347
+ z-index: 10;
1203
1348
  overflow: hidden;
1204
- display: none;
1205
- border-radius: 4px;
1349
+ display: block;
1350
+ border-radius: 6px;
1351
+ background: rgba(2, 8, 20, 0.92);
1352
+ border: 1px solid rgba(0, 212, 255, 0.25);
1353
+ box-shadow:
1354
+ 0 0 30px rgba(0, 212, 255, 0.15),
1355
+ 0 0 60px rgba(0, 212, 255, 0.05),
1356
+ inset 0 0 80px rgba(0, 0, 0, 0.5);
1357
+ backdrop-filter: blur(8px);
1358
+ cursor: grab;
1359
+ transition: box-shadow 0.3s ease;
1360
+ }
1361
+
1362
+ #code-crawl:hover {
1363
+ box-shadow:
1364
+ 0 0 40px rgba(0, 212, 255, 0.25),
1365
+ 0 0 80px rgba(0, 212, 255, 0.08),
1366
+ inset 0 0 80px rgba(0, 0, 0, 0.5);
1206
1367
  }
1207
1368
 
1208
1369
  #code-crawl.active {
1209
- display: block;
1370
+ animation: crawl-open 0.4s ease-out;
1210
1371
  }
1211
1372
 
1212
- #crawl-header {
1373
+ @keyframes crawl-open {
1374
+ 0% { opacity: 0; transform: scale(0.85); }
1375
+ 100% { opacity: 1; transform: scale(1); }
1376
+ }
1377
+
1378
+ /* Scan line sweep effect */
1379
+ #code-crawl::before {
1380
+ content: '';
1213
1381
  position: absolute;
1214
- top: 0;
1215
- left: 0; right: 0;
1216
- padding: 6px 12px;
1382
+ top: 0; left: 0; right: 0;
1383
+ height: 2px;
1384
+ background: linear-gradient(90deg, transparent, var(--accent), transparent);
1385
+ z-index: 5;
1386
+ animation: crawl-scanline 3s linear infinite;
1387
+ opacity: 0.6;
1388
+ }
1389
+
1390
+ @keyframes crawl-scanline {
1391
+ 0% { top: 0; opacity: 0.8; }
1392
+ 100% { top: 100%; opacity: 0; }
1393
+ }
1394
+
1395
+ /* Corner brackets (FBI terminal style) */
1396
+ #code-crawl::after {
1397
+ content: '';
1398
+ position: absolute;
1399
+ inset: 4px;
1400
+ border: 1px solid rgba(0, 212, 255, 0.12);
1401
+ border-radius: 3px;
1402
+ pointer-events: none;
1403
+ z-index: 1;
1404
+ }
1405
+
1406
+ /* Dragging state */
1407
+ #code-crawl.dragging {
1408
+ cursor: grabbing;
1409
+ opacity: 0.9;
1410
+ box-shadow:
1411
+ 0 0 50px rgba(0, 212, 255, 0.3),
1412
+ 0 0 100px rgba(0, 212, 255, 0.1);
1413
+ }
1414
+
1415
+ #crawl-header {
1416
+ position: relative;
1417
+ padding: 8px 12px;
1217
1418
  display: flex;
1218
1419
  justify-content: space-between;
1219
1420
  align-items: center;
1220
- z-index: 2;
1221
- background: linear-gradient(180deg, rgba(5,10,24,0.98) 60%, transparent 100%);
1421
+ z-index: 3;
1422
+ background: linear-gradient(180deg, rgba(0, 15, 35, 0.95) 0%, rgba(0, 10, 25, 0.8) 100%);
1423
+ border-bottom: 1px solid rgba(0, 212, 255, 0.15);
1424
+ cursor: grab;
1425
+ }
1426
+
1427
+ #crawl-header::before {
1428
+ content: '◉ LIVE INTERCEPT';
1429
+ font-size: 8px;
1430
+ letter-spacing: 3px;
1431
+ color: var(--risk-high);
1432
+ position: absolute;
1433
+ top: -1px;
1434
+ left: 12px;
1435
+ animation: intercept-blink 1.5s ease-in-out infinite;
1436
+ }
1437
+
1438
+ @keyframes intercept-blink {
1439
+ 0%, 100% { opacity: 1; }
1440
+ 50% { opacity: 0.3; }
1222
1441
  }
1223
1442
 
1224
1443
  #crawl-file-name {
1225
- font-size: 9px;
1444
+ font-size: 10px;
1226
1445
  color: var(--accent);
1227
- letter-spacing: 2px;
1446
+ letter-spacing: 1.5px;
1228
1447
  font-weight: 600;
1229
1448
  text-transform: uppercase;
1230
- opacity: 0.7;
1449
+ opacity: 0.8;
1450
+ margin-top: 10px;
1451
+ white-space: nowrap;
1452
+ overflow: hidden;
1453
+ text-overflow: ellipsis;
1454
+ max-width: 320px;
1231
1455
  }
1232
1456
 
1233
1457
  #crawl-status {
1234
1458
  font-size: 9px;
1235
1459
  letter-spacing: 2px;
1236
1460
  color: var(--risk-low);
1461
+ margin-top: 10px;
1237
1462
  }
1238
1463
 
1239
1464
  #crawl-status.modifying {
1240
1465
  color: var(--risk-high);
1466
+ text-shadow: 0 0 8px rgba(255, 45, 85, 0.6);
1241
1467
  animation: status-blink 0.6s ease-in-out infinite;
1242
1468
  }
1243
1469
 
@@ -1248,24 +1474,80 @@ main {
1248
1474
 
1249
1475
  #crawl-viewport {
1250
1476
  position: absolute;
1251
- top: 28px; left: 0; right: 0; bottom: 0;
1477
+ top: 42px; left: 0; right: 0; bottom: 0;
1252
1478
  overflow-y: auto;
1253
1479
  overflow-x: hidden;
1254
1480
  }
1255
1481
 
1482
+ /* Scrollbar */
1483
+ #crawl-viewport::-webkit-scrollbar { width: 6px; }
1484
+ #crawl-viewport::-webkit-scrollbar-track { background: transparent; }
1485
+ #crawl-viewport::-webkit-scrollbar-thumb { background: rgba(0, 212, 255, 0.2); border-radius: 3px; }
1486
+
1487
+ /* Top/bottom fade for cinema effect */
1488
+ #crawl-viewport::before,
1489
+ #crawl-viewport::after {
1490
+ content: '';
1491
+ position: sticky;
1492
+ display: block;
1493
+ height: 40px;
1494
+ z-index: 2;
1495
+ pointer-events: none;
1496
+ }
1497
+ #crawl-viewport::before {
1498
+ top: 0;
1499
+ background: linear-gradient(180deg, rgba(2, 8, 20, 0.95) 0%, transparent 100%);
1500
+ }
1501
+ #crawl-viewport::after {
1502
+ bottom: 0;
1503
+ background: linear-gradient(0deg, rgba(2, 8, 20, 0.95) 0%, transparent 100%);
1504
+ }
1505
+
1256
1506
  #crawl-content {
1257
- padding: 4px 8px;
1507
+ padding: 8px 10px;
1258
1508
  }
1259
1509
 
1260
- /* Individual crawl line — VS Code Dark+ style base */
1510
+ /* Individual crawl line — FBI terminal style */
1261
1511
  .crawl-line {
1262
1512
  font-family: 'Consolas', 'SF Mono', 'Fira Code', monospace;
1263
1513
  font-size: 11px;
1264
- line-height: 1.5;
1514
+ line-height: 1.6;
1265
1515
  white-space: pre;
1266
- color: #d4d4d4;
1516
+ color: rgba(200, 214, 229, 0.5);
1267
1517
  overflow: hidden;
1268
1518
  text-overflow: ellipsis;
1519
+ padding: 0 4px;
1520
+ transition: color 0.3s, background 0.3s;
1521
+ }
1522
+
1523
+ /* Non-changed lines are dim / ghostly */
1524
+ .crawl-line:not(.added):not(.removed):not(.changed) {
1525
+ color: rgba(200, 214, 229, 0.25);
1526
+ }
1527
+
1528
+ /* Changed lines glow bright */
1529
+ .crawl-line.added, .crawl-line.removed, .crawl-line.changed {
1530
+ color: #d4d4d4;
1531
+ }
1532
+
1533
+ /* Staggered fade-in for each line */
1534
+ #code-crawl.active .crawl-line {
1535
+ animation: line-materialize 0.5s ease-out both;
1536
+ }
1537
+ #code-crawl.active .crawl-line:nth-child(1) { animation-delay: 0.02s; }
1538
+ #code-crawl.active .crawl-line:nth-child(2) { animation-delay: 0.04s; }
1539
+ #code-crawl.active .crawl-line:nth-child(3) { animation-delay: 0.06s; }
1540
+ #code-crawl.active .crawl-line:nth-child(4) { animation-delay: 0.08s; }
1541
+ #code-crawl.active .crawl-line:nth-child(5) { animation-delay: 0.10s; }
1542
+ #code-crawl.active .crawl-line:nth-child(6) { animation-delay: 0.12s; }
1543
+ #code-crawl.active .crawl-line:nth-child(7) { animation-delay: 0.14s; }
1544
+ #code-crawl.active .crawl-line:nth-child(8) { animation-delay: 0.16s; }
1545
+ #code-crawl.active .crawl-line:nth-child(9) { animation-delay: 0.18s; }
1546
+ #code-crawl.active .crawl-line:nth-child(10) { animation-delay: 0.20s; }
1547
+
1548
+ @keyframes line-materialize {
1549
+ 0% { opacity: 0; transform: translateX(8px); filter: blur(2px); }
1550
+ 100% { opacity: 1; transform: translateX(0); filter: blur(0); }
1269
1551
  }
1270
1552
 
1271
1553
  .crawl-line .cl-num {
@@ -1577,12 +1859,6 @@ main {
1577
1859
  100% { background: transparent; }
1578
1860
  }
1579
1861
 
1580
- /* Crawl status when modifying */
1581
- #crawl-status.modifying {
1582
- color: var(--risk-medium);
1583
- animation: sse-pulse 0.5s infinite;
1584
- }
1585
-
1586
1862
  /* ═══════════════════════════════════════════ */
1587
1863
  /* Audit Result (post-analysis) */
1588
1864
  /* ═══════════════════════════════════════════ */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@syke1/mcp-server",
3
- "version": "1.3.8",
3
+ "version": "1.3.10",
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",