@syke1/mcp-server 1.3.9 → 1.3.11

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.
@@ -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 = [];
@@ -18,6 +18,8 @@ let crawlData = null;
18
18
  let modifyingNodes = new Set(); // nodes currently being modified by AI
19
19
  let heartbeatNodes = new Map(); // nodeId → { riskLevel, startTime, interval }
20
20
  let diffScrollAnim = null; // animation for diff scroll
21
+ let knownNodeIds = new Set(); // track existing nodes for star-birth detection
22
+ let birthAnimations = new Map(); // nodeId → { startTime, spawnPos, targetPos }
21
23
 
22
24
  const LAYER_HEX = {
23
25
  FE: "#00d4ff", BE: "#c084fc", DB: "#ff6b35",
@@ -27,12 +29,12 @@ const LAYER_HEX = {
27
29
  const LAYER_KEYS = ["FE", "BE", "DB", "API", "CONFIG", "UTIL"];
28
30
 
29
31
  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 },
32
+ FE: { x: -2500, y: 1000, z: -1000 },
33
+ BE: { x: 2500, y: 1000, z: 1000 },
34
+ DB: { x: 0, y: -2200, z: 1800 },
35
+ API: { x: 2200, y: -1200, z: -1800 },
36
+ CONFIG: { x: -2200, y: 2500, z: 1500 },
37
+ UTIL: { x: 0, y: 200, z: 0 },
36
38
  };
37
39
 
38
40
  // ═══════════════════════════════════════════
@@ -54,18 +56,23 @@ document.addEventListener("DOMContentLoaded", async () => {
54
56
 
55
57
  // Periodic server health check (catches server down even without SSE)
56
58
  let healthCheckTimer = null;
59
+ let healthFailCount = 0;
60
+ const HEALTH_FAIL_THRESHOLD = 3; // Show offline only after 3 consecutive failures
57
61
  function startHealthCheck() {
58
62
  if (healthCheckTimer) return;
59
63
  healthCheckTimer = setInterval(async () => {
60
64
  try {
61
- const res = await fetch("/api/project-info", { signal: AbortSignal.timeout(3000) });
65
+ const res = await fetch("/api/project-info", { signal: AbortSignal.timeout(10000) });
62
66
  if (res.ok) {
63
- // Server is alive if overlay was showing, hideServerOffline handles reconnection
67
+ healthFailCount = 0; // Reset on success
64
68
  }
65
69
  } catch (e) {
66
- showServerOffline();
70
+ healthFailCount++;
71
+ if (healthFailCount >= HEALTH_FAIL_THRESHOLD) {
72
+ showServerOffline();
73
+ }
67
74
  }
68
- }, 10000); // Check every 10 seconds
75
+ }, 30000); // Check every 30 seconds (not 10)
69
76
  }
70
77
 
71
78
  // ═══════════════════════════════════════════
@@ -103,20 +110,62 @@ async function loadGraph() {
103
110
  }
104
111
  hideWelcomeOverlay();
105
112
 
113
+ const isReload = Graph !== null;
114
+
115
+ // Spawn point for new nodes (top-right corner of 3D space)
116
+ const SPAWN = { x: 5000, y: 4000, z: -2000 };
117
+
118
+ // Preserve existing node positions on reload
119
+ const currentPositions = {};
120
+ if (isReload) {
121
+ const cur = Graph.graphData();
122
+ cur.nodes.forEach(n => {
123
+ currentPositions[n.id] = { x: n.x, y: n.y, z: n.z };
124
+ });
125
+ }
126
+
106
127
  const nodes = raw.nodes.map(n => {
107
128
  const layer = n.data.layer || "UTIL";
108
129
  const c = LAYER_CENTERS[layer] || LAYER_CENTERS.UTIL;
130
+ const isNew = knownNodeIds.size > 0 && !knownNodeIds.has(n.data.id);
131
+
132
+ // For existing nodes on reload: keep their current simulated position
133
+ const targetPos = currentPositions[n.data.id] || {
134
+ x: c.x + (Math.random() - 0.5) * 600,
135
+ y: c.y + (Math.random() - 0.5) * 600,
136
+ z: c.z + (Math.random() - 0.5) * 600,
137
+ };
138
+
139
+ // New node: spawn from far corner, will animate to target
140
+ if (isNew) {
141
+ console.log("[SYKE] ★ Star birth:", n.data.id);
142
+ birthAnimations.set(n.data.id, {
143
+ startTime: Date.now(),
144
+ spawnPos: { ...SPAWN },
145
+ targetPos: { ...targetPos },
146
+ duration: 2500, // 2.5s flight time
147
+ });
148
+ }
149
+
109
150
  return {
110
151
  id: n.data.id, label: n.data.label, fullPath: n.data.fullPath,
111
152
  riskLevel: n.data.riskLevel, dependentCount: n.data.dependentCount,
112
153
  lineCount: n.data.lineCount || 0, importsCount: n.data.importsCount || 0,
113
154
  depth: n.data.depth || 0, group: n.data.group,
114
155
  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,
156
+ x: isNew ? SPAWN.x : targetPos.x,
157
+ y: isNew ? SPAWN.y : targetPos.y,
158
+ z: isNew ? SPAWN.z : targetPos.z,
159
+ // Pin new nodes at SPAWN so simulation doesn't skip their animation
160
+ fx: isNew ? SPAWN.x : undefined,
161
+ fy: isNew ? SPAWN.y : undefined,
162
+ fz: isNew ? SPAWN.z : undefined,
163
+ _isNew: isNew,
118
164
  };
119
165
  });
166
+
167
+ // Update known node IDs
168
+ knownNodeIds = new Set(nodes.map(n => n.id));
120
169
  const links = raw.edges.map(e => ({ source: e.data.source, target: e.data.target }));
121
170
  graphData = { nodes, links };
122
171
 
@@ -128,6 +177,15 @@ async function loadGraph() {
128
177
  const highRisk = nodes.filter(n => n.riskLevel === "HIGH").length;
129
178
  document.getElementById("stat-high").textContent = highRisk;
130
179
 
180
+ // ── RELOAD: just update data, no graph re-creation ──
181
+ if (isReload) {
182
+ Graph.graphData(graphData);
183
+ buildLegend(layerCounts);
184
+ console.log("[SYKE] Graph updated (reload), birth animations:", birthAnimations.size);
185
+ return;
186
+ }
187
+
188
+ // ── FIRST LOAD: create new Graph instance ──
131
189
  const container = document.getElementById("3d-graph");
132
190
 
133
191
  Graph = ForceGraph3D()(container)
@@ -140,7 +198,7 @@ async function loadGraph() {
140
198
  .nodeColor(node => getNodeColor(node))
141
199
  .nodeVal(node => {
142
200
  if (!isNodeVisible(node)) return 0.001;
143
- const base = Math.max(2, Math.sqrt(node.lineCount / 3));
201
+ const base = Math.max(8, Math.sqrt(node.lineCount) * 0.8);
144
202
  const hb = heartbeatNodes.get(node.id);
145
203
  if (hb) {
146
204
  const elapsed = Date.now() - hb.startTime;
@@ -153,6 +211,14 @@ async function loadGraph() {
153
211
  const pulse = 0.5 + 0.5 * Math.sin(Date.now() / 200);
154
212
  return base * (1 + pulse * 0.4);
155
213
  }
214
+ // Star birth: start large, shrink to normal
215
+ const birth = birthAnimations.get(node.id);
216
+ if (birth) {
217
+ const t = Math.min(1, (Date.now() - birth.startTime) / birth.duration);
218
+ if (t >= 1) birthAnimations.delete(node.id);
219
+ const scale = 1 + (3 - 1) * (1 - t) * (1 - t); // ease-out: 3x → 1x
220
+ return base * scale;
221
+ }
156
222
  return base;
157
223
  })
158
224
  .nodeOpacity(1.0)
@@ -215,19 +281,44 @@ async function loadGraph() {
215
281
  node.fz = undefined;
216
282
  })
217
283
 
218
- .d3AlphaDecay(0.015)
219
- .d3VelocityDecay(0.35)
220
- .warmupTicks(100)
221
- .cooldownTicks(500)
284
+ // Star birth: animate pinned position from SPAWN → target on each tick
285
+ .onEngineTick(() => {
286
+ if (birthAnimations.size === 0) return;
287
+ const now = Date.now();
288
+ for (const [nodeId, anim] of birthAnimations) {
289
+ const node = graphData.nodes.find(n => n.id === nodeId);
290
+ if (!node) continue;
291
+ const t = Math.min(1, (now - anim.startTime) / anim.duration);
292
+ if (t < 1) {
293
+ // Ease-out cubic: fast start, gentle landing
294
+ const ease = 1 - Math.pow(1 - t, 3);
295
+ node.fx = anim.spawnPos.x + (anim.targetPos.x - anim.spawnPos.x) * ease;
296
+ node.fy = anim.spawnPos.y + (anim.targetPos.y - anim.spawnPos.y) * ease;
297
+ node.fz = anim.spawnPos.z + (anim.targetPos.z - anim.spawnPos.z) * ease;
298
+ } else {
299
+ // Animation done: unpin, let simulation fine-tune position
300
+ node.fx = undefined;
301
+ node.fy = undefined;
302
+ node.fz = undefined;
303
+ birthAnimations.delete(nodeId);
304
+ console.log("[SYKE] ★ Star birth complete:", nodeId);
305
+ }
306
+ }
307
+ })
308
+
309
+ .d3AlphaDecay(0.008)
310
+ .d3VelocityDecay(0.3)
311
+ .warmupTicks(300)
312
+ .cooldownTicks(800)
222
313
  .enablePointerInteraction(true);
223
314
 
224
- Graph.d3Force("cluster", clusterForce(0.25));
225
- Graph.d3Force("charge").strength(-25);
315
+ Graph.d3Force("cluster", clusterForce(0.015));
316
+ Graph.d3Force("charge").strength(-800);
226
317
  Graph.d3Force("link")
227
- .distance(l => srcLayer(l) === tgtLayer(l) ? 35 : 150)
228
- .strength(l => srcLayer(l) === tgtLayer(l) ? 0.8 : 0.2);
318
+ .distance(l => srcLayer(l) === tgtLayer(l) ? 250 : 900)
319
+ .strength(l => srcLayer(l) === tgtLayer(l) ? 0.2 : 0.05);
229
320
 
230
- Graph.cameraPosition({ x: 0, y: 0, z: 1600 });
321
+ Graph.cameraPosition({ x: 0, y: 0, z: 3500 });
231
322
 
232
323
  setTimeout(() => {
233
324
  try {
@@ -257,6 +348,12 @@ async function loadGraph() {
257
348
  buildLegend(layerCounts);
258
349
  createNodeLabels();
259
350
  updateLabelsLoop();
351
+
352
+ // ── Auto-start code crawl with highest-hub file on initial load ──
353
+ setTimeout(() => {
354
+ const topNode = nodes.slice().sort((a, b) => b.dependentCount - a.dependentCount)[0];
355
+ if (topNode) startCodeCrawl(topNode.id);
356
+ }, 2000);
260
357
  }
261
358
 
262
359
  // ═══════════════════════════════════════════
@@ -366,6 +463,26 @@ function getNodeColor(node) {
366
463
  return `rgb(${r},${g},${b})`;
367
464
  }
368
465
 
466
+ // Star birth: bright white → cyan → normal layer color
467
+ const birth = birthAnimations.get(node.id);
468
+ if (birth) {
469
+ const t = Math.min(1, (Date.now() - birth.startTime) / birth.duration);
470
+ const layerHex = LAYER_HEX[node.layer] || "#ff69b4";
471
+ const lr = parseInt(layerHex.slice(1,3),16);
472
+ const lg = parseInt(layerHex.slice(3,5),16);
473
+ const lb = parseInt(layerHex.slice(5,7),16);
474
+ if (t < 0.3) {
475
+ // Phase 1: white → cyan flash
476
+ const p = t / 0.3;
477
+ return `rgb(${Math.round(255*(1-p))},${255},${255})`;
478
+ } else {
479
+ // Phase 2: cyan → normal layer color (smooth blend)
480
+ const p = (t - 0.3) / 0.7;
481
+ const ease = p * p; // ease-in for gentle arrival
482
+ return `rgb(${Math.round(lr*ease)},${Math.round(255+(lg-255)*ease)},${Math.round(255+(lb-255)*ease)})`;
483
+ }
484
+ }
485
+
369
486
  // AI is modifying this node → bright pulsing white/orange
370
487
  if (modifyingNodes.has(node.id)) {
371
488
  const t = Date.now() / 200;
@@ -604,7 +721,7 @@ function handleBackgroundClick() {
604
721
  document.getElementById("code-content").innerHTML = '<p class="placeholder">Select a node to preview source code</p>';
605
722
  document.getElementById("sim-content").innerHTML = '<p class="placeholder">Select a node, then switch to SIM tab</p>';
606
723
  document.getElementById("btn-ai-analyze").disabled = true;
607
- stopCodeCrawl();
724
+ // Don't stop code crawl — keep it always visible
608
725
  refreshGraph();
609
726
  }
610
727
 
@@ -1578,10 +1695,52 @@ function stopCodeCrawl() {
1578
1695
  if (crawlUserTimer) { clearTimeout(crawlUserTimer); crawlUserTimer = null; }
1579
1696
  const vp = document.getElementById("crawl-viewport");
1580
1697
  if (vp) { vp.removeEventListener("wheel", pauseAutoScroll); vp.removeEventListener("mousedown", pauseAutoScroll); vp.removeEventListener("touchstart", pauseAutoScroll); }
1581
- document.getElementById("code-crawl").classList.remove("active");
1698
+ // Code crawl stays always visible — never hide
1582
1699
  crawlData = null;
1583
1700
  }
1584
1701
 
1702
+ // ═══════════════════════════════════════════
1703
+ // CODE CRAWL: DRAG SUPPORT
1704
+ // ═══════════════════════════════════════════
1705
+ (function initCrawlDrag() {
1706
+ let isDragging = false, startX, startY, startLeft, startTop;
1707
+ document.addEventListener("DOMContentLoaded", () => {
1708
+ const crawlEl = document.getElementById("code-crawl");
1709
+ const header = document.getElementById("crawl-header");
1710
+ if (!crawlEl || !header) return;
1711
+
1712
+ header.addEventListener("mousedown", (e) => {
1713
+ if (e.target.closest("#crawl-viewport")) return;
1714
+ isDragging = true;
1715
+ crawlEl.classList.add("dragging");
1716
+ const rect = crawlEl.getBoundingClientRect();
1717
+ const parent = crawlEl.offsetParent.getBoundingClientRect();
1718
+ startX = e.clientX;
1719
+ startY = e.clientY;
1720
+ startLeft = rect.left - parent.left;
1721
+ startTop = rect.top - parent.top;
1722
+ crawlEl.style.left = startLeft + "px";
1723
+ crawlEl.style.top = startTop + "px";
1724
+ crawlEl.style.bottom = "auto";
1725
+ e.preventDefault();
1726
+ });
1727
+
1728
+ document.addEventListener("mousemove", (e) => {
1729
+ if (!isDragging) return;
1730
+ const dx = e.clientX - startX;
1731
+ const dy = e.clientY - startY;
1732
+ crawlEl.style.left = (startLeft + dx) + "px";
1733
+ crawlEl.style.top = (startTop + dy) + "px";
1734
+ });
1735
+
1736
+ document.addEventListener("mouseup", () => {
1737
+ if (!isDragging) return;
1738
+ isDragging = false;
1739
+ crawlEl.classList.remove("dragging");
1740
+ });
1741
+ });
1742
+ })();
1743
+
1585
1744
  // ═══════════════════════════════════════════
1586
1745
  // REAL-TIME DIFF VIEWER (SSE-driven)
1587
1746
  // ═══════════════════════════════════════════
@@ -1683,6 +1842,9 @@ function showRealtimeDiff(data) {
1683
1842
 
1684
1843
  contentEl.innerHTML = html;
1685
1844
 
1845
+ // ── Mirror diff to CODE tab (VS Code style) ──
1846
+ renderCodeTabDiff(data, html);
1847
+
1686
1848
  // ── Animated diff scroll: sweep through file, pause on changes ──
1687
1849
  if (diffScrollAnim) cancelAnimationFrame(diffScrollAnim);
1688
1850
  viewport.scrollTop = 0;
@@ -1768,6 +1930,97 @@ function updateDiffWithAnalysis(analysis) {
1768
1930
  }
1769
1931
 
1770
1932
  // ═══════════════════════════════════════════
1933
+ // CODE TAB: VS Code-style real-time diff viewer
1934
+ // ═══════════════════════════════════════════
1935
+ let codeTabScrollAnim = null;
1936
+
1937
+ function renderCodeTabDiff(data, diffHtml) {
1938
+ const codeEl = document.getElementById("code-content");
1939
+ if (!codeEl) return;
1940
+
1941
+ // Build VS Code-style header
1942
+ const fileName = data.file.split("/").pop();
1943
+ const ext = fileName.split(".").pop();
1944
+ const langLabel = { ts: "TypeScript", tsx: "TypeScript React", js: "JavaScript", jsx: "JSX",
1945
+ dart: "Dart", py: "Python", go: "Go", rs: "Rust", java: "Java", cpp: "C++", rb: "Ruby",
1946
+ css: "CSS", html: "HTML", json: "JSON", md: "Markdown", yaml: "YAML", yml: "YAML" }[ext] || ext.toUpperCase();
1947
+ const changeIcon = data.type === "added" ? "A" : data.type === "deleted" ? "D" : "M";
1948
+ const changeCls = data.type === "added" ? "vsc-added" : data.type === "deleted" ? "vsc-deleted" : "vsc-modified";
1949
+
1950
+ let html = `<div class="vsc-editor">`;
1951
+ // Tab bar (like VS Code file tab)
1952
+ html += `<div class="vsc-tab-bar">`;
1953
+ html += `<div class="vsc-tab active">`;
1954
+ html += `<span class="vsc-tab-change ${changeCls}">${changeIcon}</span>`;
1955
+ html += `<span class="vsc-tab-name">${fileName}</span>`;
1956
+ html += `<span class="vsc-tab-lang">${langLabel}</span>`;
1957
+ html += `</div>`;
1958
+ html += `</div>`;
1959
+ // Breadcrumb path
1960
+ html += `<div class="vsc-breadcrumb">${data.file}</div>`;
1961
+ // Diff content area (reuse crawl-line classes)
1962
+ html += `<div class="vsc-diff-body">${diffHtml}</div>`;
1963
+ // Status bar
1964
+ const lineCount = data.newContent ? data.newContent.length : 0;
1965
+ const diffCount = data.diffCount || 0;
1966
+ html += `<div class="vsc-status-bar">`;
1967
+ html += `<span class="vsc-status-item">${langLabel}</span>`;
1968
+ html += `<span class="vsc-status-item">Ln ${lineCount}</span>`;
1969
+ html += `<span class="vsc-status-item vsc-status-diff">+${diffCount} changes</span>`;
1970
+ html += `<span class="vsc-status-item">${new Date().toLocaleTimeString()}</span>`;
1971
+ html += `</div>`;
1972
+ html += `</div>`;
1973
+
1974
+ codeEl.innerHTML = html;
1975
+
1976
+ // Auto-switch to CODE tab
1977
+ switchTab("code");
1978
+
1979
+ // Auto-scroll to first change
1980
+ if (codeTabScrollAnim) cancelAnimationFrame(codeTabScrollAnim);
1981
+ setTimeout(() => {
1982
+ const diffBody = codeEl.querySelector(".vsc-diff-body");
1983
+ const firstChange = diffBody?.querySelector(".crawl-line.added, .crawl-line.changed, .crawl-line.removed");
1984
+ if (firstChange && diffBody) {
1985
+ firstChange.scrollIntoView({ behavior: "smooth", block: "center" });
1986
+ firstChange.classList.add("diff-flash-active");
1987
+ setTimeout(() => firstChange.classList.remove("diff-flash-active"), 1200);
1988
+ }
1989
+ }, 300);
1990
+ }
1991
+
1992
+ function updateCodeTabAnalysis(analysis) {
1993
+ const statusBar = document.querySelector(".vsc-status-bar");
1994
+ if (!statusBar) return;
1995
+
1996
+ const riskColors = { CRITICAL: "#f85149", HIGH: "#ff6b6b", MEDIUM: "#e3b341", LOW: "#3fb950", SAFE: "#3fb950" };
1997
+ const color = riskColors[analysis.riskLevel] || "#888";
1998
+ statusBar.style.background = analysis.riskLevel === "CRITICAL" || analysis.riskLevel === "HIGH" ? "#6e1b1b" : "#007acc";
1999
+
2000
+ // Add risk badge to status bar
2001
+ const existing = statusBar.querySelector(".vsc-risk-badge");
2002
+ if (existing) existing.remove();
2003
+ const badge = document.createElement("span");
2004
+ badge.className = "vsc-risk-badge";
2005
+ badge.style.cssText = `background:${color};color:#fff;padding:1px 6px;border-radius:2px;font-weight:bold;font-size:10px;`;
2006
+ badge.textContent = analysis.riskLevel;
2007
+ statusBar.prepend(badge);
2008
+
2009
+ // Add AI summary banner to diff body
2010
+ const diffBody = document.querySelector(".vsc-diff-body");
2011
+ if (diffBody && analysis.summary) {
2012
+ const banner = document.createElement("div");
2013
+ banner.className = "vsc-ai-banner";
2014
+ 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;`;
2015
+ let text = `AI: ${analysis.summary}`;
2016
+ if (analysis.suggestion) text += ` — ${analysis.suggestion}`;
2017
+ banner.textContent = text;
2018
+ const firstBanner = diffBody.querySelector(".diff-banner");
2019
+ if (firstBanner) firstBanner.after(banner);
2020
+ else diffBody.prepend(banner);
2021
+ }
2022
+ }
2023
+
1771
2024
  // ═══════════════════════════════════════════
1772
2025
  // AUDIT RESULT (shown after analysis completes)
1773
2026
  // ═══════════════════════════════════════════
@@ -1798,14 +2051,7 @@ function showAuditResult(analysis) {
1798
2051
  <div class="audit-meta">${analysis.affectedNodes?.length || 0} files checked · ${analysis.analysisMs}ms</div>
1799
2052
  </div>`;
1800
2053
 
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);
2054
+ // Keep showing audit result — don't hide code crawl
1809
2055
  } else {
1810
2056
  // ── WARNING: issues found — keep showing ──
1811
2057
  headerName.textContent = analysis.file;
@@ -1962,6 +2208,7 @@ async function initSSE() {
1962
2208
  sseSource.addEventListener("connected", (e) => {
1963
2209
  const data = JSON.parse(e.data);
1964
2210
  console.log("[SYKE:SSE] Connected, cache:", data.cacheSize, "files");
2211
+ healthFailCount = 0; // Reset health failures on SSE connect
1965
2212
  updateSSEStatus("LIVE", "connected");
1966
2213
  });
1967
2214
 
@@ -2055,6 +2302,8 @@ async function initSSE() {
2055
2302
 
2056
2303
  // Update diff view with analysis risk badge
2057
2304
  updateDiffWithAnalysis(analysis);
2305
+ // Also update CODE tab with analysis result
2306
+ updateCodeTabAnalysis(analysis);
2058
2307
 
2059
2308
  addRealtimeEvent({
2060
2309
  type: "result",
@@ -2133,24 +2382,34 @@ async function initSSE() {
2133
2382
  updateSSEStatus("PROJECT LOADED", "connected");
2134
2383
  });
2135
2384
 
2385
+ let sseRetryCount = 0;
2136
2386
  sseSource.onerror = async () => {
2137
- console.warn("[SYKE:SSE] Connection error");
2138
- updateSSEStatus("OFFLINE", "offline");
2387
+ console.warn("[SYKE:SSE] Connection error, retry #" + (sseRetryCount + 1));
2139
2388
  sseSource.close();
2140
2389
  sseSource = null;
2141
- if (sseBlocked) return; // Don't reconnect if Pro-only block
2390
+ if (sseBlocked) return;
2142
2391
 
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();
2392
+ sseRetryCount++;
2393
+ updateSSEStatus("RECONNECTING...", "warning");
2394
+
2395
+ // Only show offline after 5 consecutive SSE failures
2396
+ if (sseRetryCount >= 5) {
2397
+ try {
2398
+ const probe = await fetch("/api/project-info", { signal: AbortSignal.timeout(8000) });
2399
+ if (!probe.ok) throw new Error("not ok");
2400
+ // Server alive but SSE failing — just keep retrying
2401
+ sseRetryCount = 0;
2402
+ } catch (e) {
2403
+ showServerOffline();
2404
+ }
2150
2405
  }
2151
2406
 
2407
+ // Exponential backoff: 2s, 4s, 8s, 16s, max 30s
2408
+ const delay = Math.min(2000 * Math.pow(2, sseRetryCount - 1), 30000);
2152
2409
  if (sseReconnectTimer) clearTimeout(sseReconnectTimer);
2153
- sseReconnectTimer = setTimeout(initSSE, 3000);
2410
+ sseReconnectTimer = setTimeout(() => {
2411
+ initSSE();
2412
+ }, delay);
2154
2413
  };
2155
2414
  }
2156
2415
 
@@ -2423,7 +2682,7 @@ async function loadProjectInfo() {
2423
2682
  updateLicenseBadge(info.plan, info.expiresAt);
2424
2683
  } catch (e) {
2425
2684
  console.warn("[SYKE] Failed to load project info:", e);
2426
- showServerOffline();
2685
+ // Don't immediately show offline — let health check handle it
2427
2686
  }
2428
2687
  }
2429
2688
 
@@ -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.9",
3
+ "version": "1.3.11",
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",