@syke1/mcp-server 1.0.0
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 +112 -0
- package/dist/ai/analyzer.d.ts +3 -0
- package/dist/ai/analyzer.js +120 -0
- package/dist/ai/realtime-analyzer.d.ts +20 -0
- package/dist/ai/realtime-analyzer.js +182 -0
- package/dist/graph.d.ts +13 -0
- package/dist/graph.js +105 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +518 -0
- package/dist/languages/cpp.d.ts +2 -0
- package/dist/languages/cpp.js +109 -0
- package/dist/languages/dart.d.ts +2 -0
- package/dist/languages/dart.js +162 -0
- package/dist/languages/go.d.ts +2 -0
- package/dist/languages/go.js +111 -0
- package/dist/languages/java.d.ts +2 -0
- package/dist/languages/java.js +113 -0
- package/dist/languages/plugin.d.ts +20 -0
- package/dist/languages/plugin.js +148 -0
- package/dist/languages/python.d.ts +2 -0
- package/dist/languages/python.js +129 -0
- package/dist/languages/ruby.d.ts +2 -0
- package/dist/languages/ruby.js +97 -0
- package/dist/languages/rust.d.ts +2 -0
- package/dist/languages/rust.js +121 -0
- package/dist/languages/typescript.d.ts +2 -0
- package/dist/languages/typescript.js +138 -0
- package/dist/license/validator.d.ts +23 -0
- package/dist/license/validator.js +297 -0
- package/dist/tools/analyze-impact.d.ts +23 -0
- package/dist/tools/analyze-impact.js +102 -0
- package/dist/tools/gate-build.d.ts +25 -0
- package/dist/tools/gate-build.js +243 -0
- package/dist/watcher/file-cache.d.ts +56 -0
- package/dist/watcher/file-cache.js +241 -0
- package/dist/web/public/app.js +2398 -0
- package/dist/web/public/index.html +258 -0
- package/dist/web/public/style.css +1827 -0
- package/dist/web/server.d.ts +29 -0
- package/dist/web/server.js +744 -0
- package/package.json +50 -0
|
@@ -0,0 +1,744 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
35
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
36
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
37
|
+
};
|
|
38
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
39
|
+
exports.addWarning = addWarning;
|
|
40
|
+
exports.getUnacknowledgedWarnings = getUnacknowledgedWarnings;
|
|
41
|
+
exports.acknowledgeWarnings = acknowledgeWarnings;
|
|
42
|
+
exports.getAllWarnings = getAllWarnings;
|
|
43
|
+
exports.createWebServer = createWebServer;
|
|
44
|
+
const path = __importStar(require("path"));
|
|
45
|
+
const fs = __importStar(require("fs"));
|
|
46
|
+
const express_1 = __importDefault(require("express"));
|
|
47
|
+
const plugin_1 = require("../languages/plugin");
|
|
48
|
+
const analyze_impact_1 = require("../tools/analyze-impact");
|
|
49
|
+
const analyzer_1 = require("../ai/analyzer");
|
|
50
|
+
const realtime_analyzer_1 = require("../ai/realtime-analyzer");
|
|
51
|
+
function resolveFilePath(fileArg, projectRoot, sourceDir) {
|
|
52
|
+
const srcDir = sourceDir || path.join(projectRoot, "lib");
|
|
53
|
+
const srcDirName = path.basename(srcDir); // "lib" or "src"
|
|
54
|
+
if (path.isAbsolute(fileArg))
|
|
55
|
+
return path.normalize(fileArg);
|
|
56
|
+
if (fileArg.startsWith(srcDirName + "/") || fileArg.startsWith(srcDirName + "\\")) {
|
|
57
|
+
return path.normalize(path.join(projectRoot, fileArg));
|
|
58
|
+
}
|
|
59
|
+
return path.normalize(path.join(srcDir, fileArg));
|
|
60
|
+
}
|
|
61
|
+
function classifyFile(relPath) {
|
|
62
|
+
const lower = relPath.toLowerCase();
|
|
63
|
+
const fileName = lower.split("/").pop() || "";
|
|
64
|
+
// ── Layer — try plugin-specific classification first ──
|
|
65
|
+
let layer = "UTIL";
|
|
66
|
+
const plugin = (0, plugin_1.getPluginForFile)(relPath);
|
|
67
|
+
const pluginLayer = plugin?.classifyLayer?.(relPath);
|
|
68
|
+
if (pluginLayer) {
|
|
69
|
+
layer = pluginLayer;
|
|
70
|
+
}
|
|
71
|
+
else {
|
|
72
|
+
// FE: presentation layer, UI components
|
|
73
|
+
if (lower.includes("/presentation/") ||
|
|
74
|
+
lower.includes("/widgets/") ||
|
|
75
|
+
lower.includes("/screens/") ||
|
|
76
|
+
lower.includes("/pages/") ||
|
|
77
|
+
lower.includes("web/public/") ||
|
|
78
|
+
lower.includes("components/") ||
|
|
79
|
+
fileName.endsWith("_screen.dart") ||
|
|
80
|
+
fileName.endsWith("_page.dart") ||
|
|
81
|
+
fileName.endsWith("_widget.dart") ||
|
|
82
|
+
fileName.endsWith("_dialog.dart") ||
|
|
83
|
+
fileName.endsWith("_view.dart") ||
|
|
84
|
+
fileName.endsWith("_card.dart") ||
|
|
85
|
+
fileName.endsWith("_tile.dart") ||
|
|
86
|
+
fileName.endsWith("_form.dart") ||
|
|
87
|
+
fileName.endsWith("_bottom_sheet.dart") ||
|
|
88
|
+
fileName.endsWith(".html") ||
|
|
89
|
+
fileName.endsWith(".css") ||
|
|
90
|
+
fileName.endsWith(".jsx") ||
|
|
91
|
+
fileName.endsWith(".tsx")) {
|
|
92
|
+
layer = "FE";
|
|
93
|
+
}
|
|
94
|
+
// API: network/remote layer
|
|
95
|
+
else if (lower.includes("/api/") ||
|
|
96
|
+
fileName.includes("cloud_function") ||
|
|
97
|
+
fileName.endsWith("_api.dart") ||
|
|
98
|
+
fileName.endsWith("_client.dart") ||
|
|
99
|
+
fileName.endsWith("_remote.dart") ||
|
|
100
|
+
fileName.includes("_datasource") ||
|
|
101
|
+
fileName.includes("_data_source") ||
|
|
102
|
+
lower.includes("web/server") ||
|
|
103
|
+
fileName.endsWith("server.ts") ||
|
|
104
|
+
fileName.endsWith("server.js")) {
|
|
105
|
+
layer = "API";
|
|
106
|
+
}
|
|
107
|
+
// DB: data models, entities
|
|
108
|
+
else if (lower.includes("/models/") ||
|
|
109
|
+
lower.includes("/entities/") ||
|
|
110
|
+
fileName.endsWith("_model.dart") ||
|
|
111
|
+
fileName.endsWith("_entity.dart") ||
|
|
112
|
+
fileName.endsWith("_dto.dart") ||
|
|
113
|
+
fileName.endsWith("_schema.dart") ||
|
|
114
|
+
fileName.endsWith(".model.ts") ||
|
|
115
|
+
fileName.endsWith(".entity.ts") ||
|
|
116
|
+
fileName.endsWith(".schema.ts")) {
|
|
117
|
+
layer = "DB";
|
|
118
|
+
}
|
|
119
|
+
// BE: business logic, repositories, services, state management
|
|
120
|
+
else if (lower.includes("/data/") ||
|
|
121
|
+
lower.includes("/domain/") ||
|
|
122
|
+
lower.includes("/application/") ||
|
|
123
|
+
lower.includes("/providers/") ||
|
|
124
|
+
lower.includes("/notifiers/") ||
|
|
125
|
+
lower.includes("tools/") ||
|
|
126
|
+
lower.includes("ai/") ||
|
|
127
|
+
lower.includes("watcher/") ||
|
|
128
|
+
fileName.endsWith("_repository.dart") ||
|
|
129
|
+
fileName.endsWith("_service.dart") ||
|
|
130
|
+
fileName.endsWith("_provider.dart") ||
|
|
131
|
+
fileName.endsWith("_notifier.dart") ||
|
|
132
|
+
fileName.endsWith("_controller.dart") ||
|
|
133
|
+
fileName.endsWith("_usecase.dart") ||
|
|
134
|
+
fileName.endsWith("_state.dart") ||
|
|
135
|
+
fileName.endsWith("_bloc.dart") ||
|
|
136
|
+
fileName.endsWith("_cubit.dart") ||
|
|
137
|
+
fileName.endsWith(".service.ts") ||
|
|
138
|
+
fileName.endsWith(".controller.ts")) {
|
|
139
|
+
layer = "BE";
|
|
140
|
+
}
|
|
141
|
+
// CONFIG: app config, themes, routing, constants
|
|
142
|
+
else if (lower.includes("/config/") ||
|
|
143
|
+
lower.includes("/theme/") ||
|
|
144
|
+
lower.includes("/router/") ||
|
|
145
|
+
lower.includes("/routing/") ||
|
|
146
|
+
lower.startsWith("app/") ||
|
|
147
|
+
fileName.endsWith("_config.dart") ||
|
|
148
|
+
fileName.endsWith("_theme.dart") ||
|
|
149
|
+
fileName.endsWith("_constants.dart") ||
|
|
150
|
+
fileName.endsWith("_routes.dart") ||
|
|
151
|
+
fileName === "main.dart" ||
|
|
152
|
+
fileName === "index.ts" ||
|
|
153
|
+
fileName === "index.js" ||
|
|
154
|
+
fileName.endsWith(".config.ts") ||
|
|
155
|
+
fileName.endsWith(".config.js")) {
|
|
156
|
+
layer = "CONFIG";
|
|
157
|
+
}
|
|
158
|
+
// UTIL: utilities, helpers, extensions, shared
|
|
159
|
+
else if (lower.includes("/utils/") ||
|
|
160
|
+
lower.includes("/helpers/") ||
|
|
161
|
+
lower.includes("/extensions/") ||
|
|
162
|
+
lower.includes("shared/") ||
|
|
163
|
+
lower.includes("languages/") ||
|
|
164
|
+
fileName.endsWith("_util.dart") ||
|
|
165
|
+
fileName.endsWith("_helper.dart") ||
|
|
166
|
+
fileName.endsWith("_extension.dart") ||
|
|
167
|
+
fileName.endsWith("_mixin.dart") ||
|
|
168
|
+
fileName.endsWith(".util.ts") ||
|
|
169
|
+
fileName.endsWith(".helper.ts")) {
|
|
170
|
+
layer = "UTIL";
|
|
171
|
+
}
|
|
172
|
+
} // end plugin fallback
|
|
173
|
+
// ── Action (infer from filename patterns) ──
|
|
174
|
+
let action = "X";
|
|
175
|
+
if (fileName.includes("create") || fileName.includes("add") || fileName.includes("register") || fileName.includes("new")) {
|
|
176
|
+
action = "C";
|
|
177
|
+
}
|
|
178
|
+
else if (fileName.includes("list") || fileName.includes("get") || fileName.includes("fetch") || fileName.includes("show") || fileName.includes("detail") || fileName.includes("_screen") || fileName.includes("_page")) {
|
|
179
|
+
action = "R";
|
|
180
|
+
}
|
|
181
|
+
else if (fileName.includes("update") || fileName.includes("edit") || fileName.includes("modify") || fileName.includes("change")) {
|
|
182
|
+
action = "U";
|
|
183
|
+
}
|
|
184
|
+
else if (fileName.includes("delete") || fileName.includes("remove")) {
|
|
185
|
+
action = "D";
|
|
186
|
+
}
|
|
187
|
+
// ── Environment ──
|
|
188
|
+
const env = (fileName.includes("_test") || fileName.includes("_mock") || fileName.includes("_fake") || lower.includes("/test/")) ? "DEV" : "PROD";
|
|
189
|
+
return { layer, action, env };
|
|
190
|
+
}
|
|
191
|
+
const warningStore = [];
|
|
192
|
+
const MAX_WARNINGS = 50;
|
|
193
|
+
function addWarning(analysis) {
|
|
194
|
+
// Only store non-SAFE warnings
|
|
195
|
+
if (analysis.riskLevel === "SAFE")
|
|
196
|
+
return;
|
|
197
|
+
warningStore.unshift({
|
|
198
|
+
file: analysis.file,
|
|
199
|
+
riskLevel: analysis.riskLevel,
|
|
200
|
+
summary: analysis.summary,
|
|
201
|
+
brokenImports: analysis.brokenImports,
|
|
202
|
+
sideEffects: analysis.sideEffects,
|
|
203
|
+
warnings: analysis.warnings,
|
|
204
|
+
suggestion: analysis.suggestion,
|
|
205
|
+
affectedCount: analysis.affectedNodes.length,
|
|
206
|
+
timestamp: analysis.timestamp,
|
|
207
|
+
acknowledged: false,
|
|
208
|
+
});
|
|
209
|
+
// Cap the store
|
|
210
|
+
while (warningStore.length > MAX_WARNINGS)
|
|
211
|
+
warningStore.pop();
|
|
212
|
+
}
|
|
213
|
+
function getUnacknowledgedWarnings() {
|
|
214
|
+
return warningStore.filter(w => !w.acknowledged);
|
|
215
|
+
}
|
|
216
|
+
function acknowledgeWarnings() {
|
|
217
|
+
let count = 0;
|
|
218
|
+
for (const w of warningStore) {
|
|
219
|
+
if (!w.acknowledged) {
|
|
220
|
+
w.acknowledged = true;
|
|
221
|
+
count++;
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
return count;
|
|
225
|
+
}
|
|
226
|
+
function getAllWarnings() {
|
|
227
|
+
return [...warningStore];
|
|
228
|
+
}
|
|
229
|
+
function createWebServer(getGraphFn, fileCache, switchProjectFn, getProjectRoot, getPackageName, getLicenseStatus) {
|
|
230
|
+
const app = (0, express_1.default)();
|
|
231
|
+
app.use(express_1.default.json());
|
|
232
|
+
// Serve static files from public/
|
|
233
|
+
const publicDir = path.join(__dirname, "public");
|
|
234
|
+
app.use(express_1.default.static(publicDir));
|
|
235
|
+
// ── SSE: Server-Sent Events for real-time updates ──
|
|
236
|
+
const sseClients = new Set();
|
|
237
|
+
function broadcastSSE(event, data) {
|
|
238
|
+
const payload = `event: ${event}\ndata: ${JSON.stringify(data)}\n\n`;
|
|
239
|
+
for (const client of sseClients) {
|
|
240
|
+
try {
|
|
241
|
+
client.write(payload);
|
|
242
|
+
}
|
|
243
|
+
catch (_) {
|
|
244
|
+
sseClients.delete(client);
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
app.get("/api/events", (_req, res) => {
|
|
249
|
+
res.writeHead(200, {
|
|
250
|
+
"Content-Type": "text/event-stream",
|
|
251
|
+
"Cache-Control": "no-cache",
|
|
252
|
+
"Connection": "keep-alive",
|
|
253
|
+
"Access-Control-Allow-Origin": "*",
|
|
254
|
+
});
|
|
255
|
+
// Send initial connection event
|
|
256
|
+
res.write(`event: connected\ndata: ${JSON.stringify({ clients: sseClients.size + 1, cacheSize: fileCache?.size || 0 })}\n\n`);
|
|
257
|
+
sseClients.add(res);
|
|
258
|
+
console.error(`[syke:sse] Client connected (${sseClients.size} total)`);
|
|
259
|
+
_req.on("close", () => {
|
|
260
|
+
sseClients.delete(res);
|
|
261
|
+
console.error(`[syke:sse] Client disconnected (${sseClients.size} total)`);
|
|
262
|
+
});
|
|
263
|
+
});
|
|
264
|
+
// Wire FileCache change events → SSE broadcast + AI analysis
|
|
265
|
+
if (fileCache) {
|
|
266
|
+
fileCache.on("change", async (change) => {
|
|
267
|
+
const graph = getGraphFn();
|
|
268
|
+
const absPath = path.normalize(path.join(graph.sourceDir, change.relativePath));
|
|
269
|
+
// Compute affected nodes for visual pulse
|
|
270
|
+
const revDeps = graph.reverse.get(absPath) || [];
|
|
271
|
+
const fwdDeps = graph.forward.get(absPath) || [];
|
|
272
|
+
const connectedNodes = [...new Set([...revDeps, ...fwdDeps])].map(f => path.relative(graph.sourceDir, f).replace(/\\/g, "/"));
|
|
273
|
+
// Send diff lines (capped at 100 for bandwidth)
|
|
274
|
+
const diffLines = change.diff.slice(0, 100).map(d => ({
|
|
275
|
+
line: d.line,
|
|
276
|
+
type: d.type,
|
|
277
|
+
old: d.old,
|
|
278
|
+
new: d.new,
|
|
279
|
+
}));
|
|
280
|
+
// Send new file content (for code crawl display)
|
|
281
|
+
const newLines = change.newContent
|
|
282
|
+
? change.newContent.split("\n").slice(0, 300)
|
|
283
|
+
: [];
|
|
284
|
+
// Immediately broadcast the file change event (node pulse starts)
|
|
285
|
+
broadcastSSE("file-change", {
|
|
286
|
+
file: change.relativePath,
|
|
287
|
+
type: change.type,
|
|
288
|
+
diffCount: change.diff.length,
|
|
289
|
+
diff: diffLines,
|
|
290
|
+
newContent: newLines,
|
|
291
|
+
connectedNodes,
|
|
292
|
+
timestamp: change.timestamp,
|
|
293
|
+
});
|
|
294
|
+
// Run Gemini real-time analysis (Pro only)
|
|
295
|
+
const license = getLicenseStatus?.();
|
|
296
|
+
if (license && license.plan === "pro") {
|
|
297
|
+
broadcastSSE("analysis-start", { file: change.relativePath });
|
|
298
|
+
try {
|
|
299
|
+
const analysis = await (0, realtime_analyzer_1.analyzeChangeRealtime)(change, graph, (relPath) => fileCache.getFileByRelPath(relPath));
|
|
300
|
+
broadcastSSE("analysis-result", analysis);
|
|
301
|
+
// Store warnings for MCP check_warnings tool
|
|
302
|
+
addWarning(analysis);
|
|
303
|
+
// If graph structure changed (new/deleted files), rebuild
|
|
304
|
+
if (change.type === "added" || change.type === "deleted") {
|
|
305
|
+
broadcastSSE("graph-rebuild", { reason: change.type, file: change.relativePath });
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
catch (err) {
|
|
309
|
+
broadcastSSE("analysis-error", {
|
|
310
|
+
file: change.relativePath,
|
|
311
|
+
error: err.message,
|
|
312
|
+
});
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
else {
|
|
316
|
+
// Free: still rebuild graph on structural changes, but skip AI
|
|
317
|
+
if (change.type === "added" || change.type === "deleted") {
|
|
318
|
+
broadcastSSE("graph-rebuild", { reason: change.type, file: change.relativePath });
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
});
|
|
322
|
+
}
|
|
323
|
+
// GET /api/cache-status — Memory cache stats
|
|
324
|
+
app.get("/api/cache-status", (_req, res) => {
|
|
325
|
+
if (!fileCache) {
|
|
326
|
+
return res.json({ enabled: false });
|
|
327
|
+
}
|
|
328
|
+
res.json({
|
|
329
|
+
enabled: true,
|
|
330
|
+
fileCount: fileCache.size,
|
|
331
|
+
totalLines: fileCache.totalLines,
|
|
332
|
+
sseClients: sseClients.size,
|
|
333
|
+
});
|
|
334
|
+
});
|
|
335
|
+
// GET /api/graph — Cytoscape.js compatible JSON
|
|
336
|
+
app.get("/api/graph", (_req, res) => {
|
|
337
|
+
const graph = getGraphFn();
|
|
338
|
+
const nodes = [];
|
|
339
|
+
const edges = [];
|
|
340
|
+
// ── Compute depth for each file (BFS from roots) ──
|
|
341
|
+
const depthMap = new Map();
|
|
342
|
+
const roots = [...graph.files].filter(f => {
|
|
343
|
+
const rev = graph.reverse.get(f);
|
|
344
|
+
return !rev || rev.length === 0;
|
|
345
|
+
});
|
|
346
|
+
const queue = roots.map(r => [r, 0]);
|
|
347
|
+
while (queue.length > 0) {
|
|
348
|
+
const [file, d] = queue.shift();
|
|
349
|
+
if (depthMap.has(file))
|
|
350
|
+
continue;
|
|
351
|
+
depthMap.set(file, d);
|
|
352
|
+
const fwdDeps = graph.forward.get(file) || [];
|
|
353
|
+
for (const dep of fwdDeps) {
|
|
354
|
+
if (!depthMap.has(dep))
|
|
355
|
+
queue.push([dep, d + 1]);
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
for (const file of graph.files) {
|
|
359
|
+
const rel = path.relative(graph.sourceDir, file).replace(/\\/g, "/");
|
|
360
|
+
const revDeps = graph.reverse.get(file) || [];
|
|
361
|
+
const dependentCount = revDeps.length;
|
|
362
|
+
const riskLevel = (0, analyze_impact_1.classifyRisk)(dependentCount);
|
|
363
|
+
const parts = rel.split("/");
|
|
364
|
+
const group = parts.length > 1 ? parts[0] + "/" + parts[1] : parts[0];
|
|
365
|
+
const { layer, action, env } = classifyFile(rel);
|
|
366
|
+
// Count lines
|
|
367
|
+
let lineCount = 0;
|
|
368
|
+
try {
|
|
369
|
+
const content = fs.readFileSync(file, "utf-8");
|
|
370
|
+
lineCount = content.split("\n").length;
|
|
371
|
+
}
|
|
372
|
+
catch (_) { }
|
|
373
|
+
// Imports count (direct forward dependencies)
|
|
374
|
+
const importsCount = (graph.forward.get(file) || []).length;
|
|
375
|
+
// Depth in dependency tree
|
|
376
|
+
const depth = depthMap.get(file) ?? 0;
|
|
377
|
+
nodes.push({
|
|
378
|
+
data: {
|
|
379
|
+
id: rel,
|
|
380
|
+
label: parts[parts.length - 1],
|
|
381
|
+
fullPath: rel,
|
|
382
|
+
riskLevel,
|
|
383
|
+
dependentCount,
|
|
384
|
+
lineCount,
|
|
385
|
+
importsCount,
|
|
386
|
+
depth,
|
|
387
|
+
group,
|
|
388
|
+
layer,
|
|
389
|
+
action,
|
|
390
|
+
env,
|
|
391
|
+
},
|
|
392
|
+
});
|
|
393
|
+
}
|
|
394
|
+
for (const [file, deps] of graph.forward) {
|
|
395
|
+
const from = path.relative(graph.sourceDir, file).replace(/\\/g, "/");
|
|
396
|
+
for (const d of deps) {
|
|
397
|
+
const to = path.relative(graph.sourceDir, d).replace(/\\/g, "/");
|
|
398
|
+
edges.push({ data: { source: from, target: to } });
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
res.json({ nodes, edges });
|
|
402
|
+
});
|
|
403
|
+
// GET /api/impact/:file — Impact analysis for a specific file
|
|
404
|
+
app.get("/api/impact/*splat", (req, res) => {
|
|
405
|
+
const splat = req.params.splat;
|
|
406
|
+
const fileParam = Array.isArray(splat) ? splat.join("/") : splat;
|
|
407
|
+
if (!fileParam) {
|
|
408
|
+
return res.status(400).json({ error: "File path required" });
|
|
409
|
+
}
|
|
410
|
+
const graph = getGraphFn();
|
|
411
|
+
const resolved = resolveFilePath(fileParam, graph.projectRoot, graph.sourceDir);
|
|
412
|
+
if (!graph.files.has(resolved)) {
|
|
413
|
+
return res.status(404).json({ error: `File not found in graph: ${fileParam}` });
|
|
414
|
+
}
|
|
415
|
+
const result = (0, analyze_impact_1.analyzeImpact)(resolved, graph);
|
|
416
|
+
res.json(result);
|
|
417
|
+
});
|
|
418
|
+
// POST /api/ai-analyze — Gemini AI semantic analysis (Pro only)
|
|
419
|
+
app.post("/api/ai-analyze", async (req, res) => {
|
|
420
|
+
// License check — Pro only
|
|
421
|
+
const license = getLicenseStatus?.();
|
|
422
|
+
if (!license || license.plan !== "pro") {
|
|
423
|
+
return res.status(403).json({
|
|
424
|
+
error: "AI analysis requires SYKE Pro. Upgrade at https://syke.cloud/dashboard/",
|
|
425
|
+
requiresPro: true,
|
|
426
|
+
});
|
|
427
|
+
}
|
|
428
|
+
const { file } = req.body;
|
|
429
|
+
if (!file) {
|
|
430
|
+
return res.status(400).json({ error: "file is required in body" });
|
|
431
|
+
}
|
|
432
|
+
const graph = getGraphFn();
|
|
433
|
+
const resolved = resolveFilePath(file, graph.projectRoot, graph.sourceDir);
|
|
434
|
+
if (!graph.files.has(resolved)) {
|
|
435
|
+
return res.status(404).json({ error: `File not found in graph: ${file}` });
|
|
436
|
+
}
|
|
437
|
+
const impactResult = (0, analyze_impact_1.analyzeImpact)(resolved, graph);
|
|
438
|
+
try {
|
|
439
|
+
const aiResult = await (0, analyzer_1.analyzeWithAI)(resolved, impactResult, graph);
|
|
440
|
+
res.json({ file: impactResult.relativePath, analysis: aiResult });
|
|
441
|
+
}
|
|
442
|
+
catch (err) {
|
|
443
|
+
res.status(500).json({ error: err.message || "AI analysis failed" });
|
|
444
|
+
}
|
|
445
|
+
});
|
|
446
|
+
// GET /api/hub-files — Top hub files ranking (Free: top 3, Pro: unlimited)
|
|
447
|
+
app.get("/api/hub-files", (req, res) => {
|
|
448
|
+
const requested = parseInt(req.query.top) || 10;
|
|
449
|
+
const license = getLicenseStatus?.();
|
|
450
|
+
const isPro = license?.plan === "pro";
|
|
451
|
+
const top = isPro ? requested : Math.min(requested, 3);
|
|
452
|
+
const graph = getGraphFn();
|
|
453
|
+
const hubs = (0, analyze_impact_1.getHubFiles)(graph, top);
|
|
454
|
+
res.json({ hubs, totalFiles: graph.files.size, limited: !isPro, plan: license?.plan || "free" });
|
|
455
|
+
});
|
|
456
|
+
// POST /api/connected-code — Batch load code from file + connected nodes
|
|
457
|
+
app.post("/api/connected-code", (req, res) => {
|
|
458
|
+
const { file, maxFiles = 6, maxLinesPerFile = 80 } = req.body;
|
|
459
|
+
if (!file)
|
|
460
|
+
return res.status(400).json({ error: "file required" });
|
|
461
|
+
const graph = getGraphFn();
|
|
462
|
+
const resolved = resolveFilePath(file, graph.projectRoot, graph.sourceDir);
|
|
463
|
+
const toRel = (f) => path.relative(graph.sourceDir, f).replace(/\\/g, "/");
|
|
464
|
+
if (!graph.files.has(resolved)) {
|
|
465
|
+
return res.status(404).json({ error: `File not found: ${file}` });
|
|
466
|
+
}
|
|
467
|
+
// Gather: selected file + direct dependents + direct imports
|
|
468
|
+
const filesToLoad = [resolved];
|
|
469
|
+
const revDeps = graph.reverse.get(resolved) || [];
|
|
470
|
+
const fwdDeps = graph.forward.get(resolved) || [];
|
|
471
|
+
for (const d of [...revDeps, ...fwdDeps]) {
|
|
472
|
+
if (!filesToLoad.includes(d))
|
|
473
|
+
filesToLoad.push(d);
|
|
474
|
+
if (filesToLoad.length >= maxFiles)
|
|
475
|
+
break;
|
|
476
|
+
}
|
|
477
|
+
const results = [];
|
|
478
|
+
for (const f of filesToLoad) {
|
|
479
|
+
try {
|
|
480
|
+
const content = fs.readFileSync(f, "utf-8");
|
|
481
|
+
const allLines = content.split("\n");
|
|
482
|
+
const rel = toRel(f);
|
|
483
|
+
const { layer } = classifyFile(rel);
|
|
484
|
+
results.push({
|
|
485
|
+
path: rel,
|
|
486
|
+
layer,
|
|
487
|
+
lines: allLines.slice(0, maxLinesPerFile),
|
|
488
|
+
lineCount: allLines.length,
|
|
489
|
+
});
|
|
490
|
+
}
|
|
491
|
+
catch (_) { }
|
|
492
|
+
}
|
|
493
|
+
res.json({ files: results });
|
|
494
|
+
});
|
|
495
|
+
// GET /api/file-content/:file — Source code preview
|
|
496
|
+
app.get("/api/file-content/*splat", (req, res) => {
|
|
497
|
+
const splat = req.params.splat;
|
|
498
|
+
const fileParam = Array.isArray(splat) ? splat.join("/") : splat;
|
|
499
|
+
if (!fileParam)
|
|
500
|
+
return res.status(400).json({ error: "File path required" });
|
|
501
|
+
const graph = getGraphFn();
|
|
502
|
+
const resolved = resolveFilePath(fileParam, graph.projectRoot, graph.sourceDir);
|
|
503
|
+
if (!graph.files.has(resolved)) {
|
|
504
|
+
return res.status(404).json({ error: `File not found: ${fileParam}` });
|
|
505
|
+
}
|
|
506
|
+
try {
|
|
507
|
+
const content = fs.readFileSync(resolved, "utf-8");
|
|
508
|
+
const lines = content.split("\n");
|
|
509
|
+
res.json({
|
|
510
|
+
path: fileParam,
|
|
511
|
+
lineCount: lines.length,
|
|
512
|
+
content: lines.length > 500 ? lines.slice(0, 500).join("\n") + "\n// ... truncated ..." : content,
|
|
513
|
+
truncated: lines.length > 500,
|
|
514
|
+
});
|
|
515
|
+
}
|
|
516
|
+
catch (err) {
|
|
517
|
+
res.status(500).json({ error: err.message });
|
|
518
|
+
}
|
|
519
|
+
});
|
|
520
|
+
// GET /api/cycles — Detect circular dependencies
|
|
521
|
+
app.get("/api/cycles", (_req, res) => {
|
|
522
|
+
const graph = getGraphFn();
|
|
523
|
+
const toRel = (f) => path.relative(graph.sourceDir, f).replace(/\\/g, "/");
|
|
524
|
+
const cycles = [];
|
|
525
|
+
const visited = new Set();
|
|
526
|
+
const stack = new Set();
|
|
527
|
+
const parent = new Map();
|
|
528
|
+
function dfs(file, pathSoFar) {
|
|
529
|
+
if (cycles.length >= 50)
|
|
530
|
+
return; // cap
|
|
531
|
+
visited.add(file);
|
|
532
|
+
stack.add(file);
|
|
533
|
+
const deps = graph.forward.get(file) || [];
|
|
534
|
+
for (const dep of deps) {
|
|
535
|
+
if (stack.has(dep)) {
|
|
536
|
+
// Found cycle — extract it
|
|
537
|
+
const cycleStart = pathSoFar.indexOf(dep);
|
|
538
|
+
if (cycleStart >= 0) {
|
|
539
|
+
const cycle = pathSoFar.slice(cycleStart).map(toRel);
|
|
540
|
+
cycle.push(toRel(dep)); // close the loop
|
|
541
|
+
cycles.push(cycle);
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
else if (!visited.has(dep)) {
|
|
545
|
+
dfs(dep, [...pathSoFar, dep]);
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
stack.delete(file);
|
|
549
|
+
}
|
|
550
|
+
for (const file of graph.files) {
|
|
551
|
+
if (!visited.has(file)) {
|
|
552
|
+
dfs(file, [file]);
|
|
553
|
+
}
|
|
554
|
+
}
|
|
555
|
+
res.json({ cycles, count: cycles.length });
|
|
556
|
+
});
|
|
557
|
+
// GET /api/shortest-path?from=X&to=Y — BFS shortest path (follows forward edges)
|
|
558
|
+
app.get("/api/shortest-path", (req, res) => {
|
|
559
|
+
const fromParam = req.query.from;
|
|
560
|
+
const toParam = req.query.to;
|
|
561
|
+
if (!fromParam || !toParam)
|
|
562
|
+
return res.status(400).json({ error: "from and to required" });
|
|
563
|
+
const graph = getGraphFn();
|
|
564
|
+
const toRel = (f) => path.relative(graph.sourceDir, f).replace(/\\/g, "/");
|
|
565
|
+
const fromAbs = resolveFilePath(fromParam, graph.projectRoot, graph.sourceDir);
|
|
566
|
+
const toAbs = resolveFilePath(toParam, graph.projectRoot, graph.sourceDir);
|
|
567
|
+
if (!graph.files.has(fromAbs) || !graph.files.has(toAbs)) {
|
|
568
|
+
return res.status(404).json({ error: "File not found in graph" });
|
|
569
|
+
}
|
|
570
|
+
// BFS on combined forward + reverse (undirected shortest path)
|
|
571
|
+
const prev = new Map();
|
|
572
|
+
const visited = new Set();
|
|
573
|
+
const queue = [fromAbs];
|
|
574
|
+
visited.add(fromAbs);
|
|
575
|
+
let found = false;
|
|
576
|
+
while (queue.length > 0 && !found) {
|
|
577
|
+
const cur = queue.shift();
|
|
578
|
+
const neighbors = new Set();
|
|
579
|
+
(graph.forward.get(cur) || []).forEach(n => neighbors.add(n));
|
|
580
|
+
(graph.reverse.get(cur) || []).forEach(n => neighbors.add(n));
|
|
581
|
+
for (const nb of neighbors) {
|
|
582
|
+
if (!visited.has(nb)) {
|
|
583
|
+
visited.add(nb);
|
|
584
|
+
prev.set(nb, cur);
|
|
585
|
+
if (nb === toAbs) {
|
|
586
|
+
found = true;
|
|
587
|
+
break;
|
|
588
|
+
}
|
|
589
|
+
queue.push(nb);
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
if (!found)
|
|
594
|
+
return res.json({ path: [], distance: -1 });
|
|
595
|
+
// Reconstruct path
|
|
596
|
+
const pathResult = [];
|
|
597
|
+
let cur = toAbs;
|
|
598
|
+
while (cur) {
|
|
599
|
+
pathResult.unshift(toRel(cur));
|
|
600
|
+
cur = prev.get(cur);
|
|
601
|
+
}
|
|
602
|
+
res.json({ path: pathResult, distance: pathResult.length - 1 });
|
|
603
|
+
});
|
|
604
|
+
// GET /api/simulate-delete/:file — Simulate file removal
|
|
605
|
+
app.get("/api/simulate-delete/*splat", (req, res) => {
|
|
606
|
+
const splat = req.params.splat;
|
|
607
|
+
const fileParam = Array.isArray(splat) ? splat.join("/") : splat;
|
|
608
|
+
if (!fileParam)
|
|
609
|
+
return res.status(400).json({ error: "File path required" });
|
|
610
|
+
const graph = getGraphFn();
|
|
611
|
+
const resolved = resolveFilePath(fileParam, graph.projectRoot, graph.sourceDir);
|
|
612
|
+
const toRel = (f) => path.relative(graph.sourceDir, f).replace(/\\/g, "/");
|
|
613
|
+
if (!graph.files.has(resolved)) {
|
|
614
|
+
return res.status(404).json({ error: `File not found: ${fileParam}` });
|
|
615
|
+
}
|
|
616
|
+
// Files that directly import the deleted file → will have broken imports
|
|
617
|
+
const brokenImports = (graph.reverse.get(resolved) || []).map(toRel);
|
|
618
|
+
// Full cascade: all transitively affected files
|
|
619
|
+
const cascadeSet = new Set();
|
|
620
|
+
const queue = [...(graph.reverse.get(resolved) || [])];
|
|
621
|
+
for (const q of queue)
|
|
622
|
+
cascadeSet.add(q);
|
|
623
|
+
while (queue.length > 0) {
|
|
624
|
+
const cur = queue.shift();
|
|
625
|
+
for (const dep of (graph.reverse.get(cur) || [])) {
|
|
626
|
+
if (!cascadeSet.has(dep) && dep !== resolved) {
|
|
627
|
+
cascadeSet.add(dep);
|
|
628
|
+
queue.push(dep);
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
// Orphaned files: files that only had forward deps to the deleted file
|
|
633
|
+
const orphaned = [];
|
|
634
|
+
for (const dep of (graph.forward.get(resolved) || [])) {
|
|
635
|
+
const revDeps = graph.reverse.get(dep) || [];
|
|
636
|
+
// If the deleted file is the only one importing this dep
|
|
637
|
+
if (revDeps.length === 1 && revDeps[0] === resolved) {
|
|
638
|
+
orphaned.push(toRel(dep));
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
res.json({
|
|
642
|
+
deletedFile: fileParam,
|
|
643
|
+
brokenImports,
|
|
644
|
+
brokenCount: brokenImports.length,
|
|
645
|
+
cascadeFiles: [...cascadeSet].map(toRel),
|
|
646
|
+
cascadeCount: cascadeSet.size,
|
|
647
|
+
orphanedFiles: orphaned,
|
|
648
|
+
orphanedCount: orphaned.length,
|
|
649
|
+
severity: cascadeSet.size >= 20 ? "CRITICAL" : cascadeSet.size >= 10 ? "HIGH" : cascadeSet.size >= 5 ? "MEDIUM" : "LOW",
|
|
650
|
+
});
|
|
651
|
+
});
|
|
652
|
+
// GET /api/warnings — List unresolved warnings (for MCP/dashboard)
|
|
653
|
+
app.get("/api/warnings", (_req, res) => {
|
|
654
|
+
const unacked = getUnacknowledgedWarnings();
|
|
655
|
+
const all = getAllWarnings();
|
|
656
|
+
res.json({
|
|
657
|
+
unresolved: unacked,
|
|
658
|
+
unresolvedCount: unacked.length,
|
|
659
|
+
totalCount: all.length,
|
|
660
|
+
});
|
|
661
|
+
});
|
|
662
|
+
// POST /api/warnings/acknowledge — Mark all warnings as acknowledged
|
|
663
|
+
app.post("/api/warnings/acknowledge", (_req, res) => {
|
|
664
|
+
const count = acknowledgeWarnings();
|
|
665
|
+
res.json({ acknowledged: count });
|
|
666
|
+
});
|
|
667
|
+
// GET /api/project-info — Current project metadata
|
|
668
|
+
app.get("/api/project-info", (_req, res) => {
|
|
669
|
+
const graph = getGraphFn();
|
|
670
|
+
let edgeCount = 0;
|
|
671
|
+
for (const deps of graph.forward.values())
|
|
672
|
+
edgeCount += deps.length;
|
|
673
|
+
const license = getLicenseStatus?.();
|
|
674
|
+
res.json({
|
|
675
|
+
projectRoot: getProjectRoot ? getProjectRoot() : graph.projectRoot,
|
|
676
|
+
packageName: getPackageName ? getPackageName() : "",
|
|
677
|
+
languages: graph.languages,
|
|
678
|
+
fileCount: graph.files.size,
|
|
679
|
+
edgeCount,
|
|
680
|
+
plan: license?.plan || "free",
|
|
681
|
+
freeFileLimit: 50,
|
|
682
|
+
});
|
|
683
|
+
});
|
|
684
|
+
// GET /api/browse-dirs — List subdirectories for folder browser
|
|
685
|
+
app.get("/api/browse-dirs", (req, res) => {
|
|
686
|
+
const dirPath = req.query.path || (process.platform === "win32" ? "C:\\" : "/");
|
|
687
|
+
const normalized = path.normalize(dirPath);
|
|
688
|
+
if (!fs.existsSync(normalized) || !fs.statSync(normalized).isDirectory()) {
|
|
689
|
+
return res.status(400).json({ error: `Not a directory: ${normalized}` });
|
|
690
|
+
}
|
|
691
|
+
try {
|
|
692
|
+
const entries = fs.readdirSync(normalized, { withFileTypes: true });
|
|
693
|
+
const dirs = entries
|
|
694
|
+
.filter(e => {
|
|
695
|
+
if (!e.isDirectory())
|
|
696
|
+
return false;
|
|
697
|
+
const name = e.name;
|
|
698
|
+
// Hide system/hidden dirs
|
|
699
|
+
if (name.startsWith(".") || name === "node_modules" || name === "$RECYCLE.BIN" || name === "System Volume Information")
|
|
700
|
+
return false;
|
|
701
|
+
return true;
|
|
702
|
+
})
|
|
703
|
+
.map(e => e.name)
|
|
704
|
+
.sort((a, b) => a.toLowerCase().localeCompare(b.toLowerCase()));
|
|
705
|
+
// Check if this looks like a project root (has package.json, pubspec.yaml, etc.)
|
|
706
|
+
const markers = ["package.json", "pubspec.yaml", "Cargo.toml", "go.mod", "pom.xml", "build.gradle", "CMakeLists.txt", "Makefile", "pyproject.toml", "setup.py"];
|
|
707
|
+
const isProject = markers.some(m => fs.existsSync(path.join(normalized, m)));
|
|
708
|
+
const detectedMarker = markers.find(m => fs.existsSync(path.join(normalized, m))) || null;
|
|
709
|
+
res.json({
|
|
710
|
+
current: normalized,
|
|
711
|
+
parent: path.dirname(normalized) !== normalized ? path.dirname(normalized) : null,
|
|
712
|
+
dirs,
|
|
713
|
+
isProject,
|
|
714
|
+
detectedMarker,
|
|
715
|
+
});
|
|
716
|
+
}
|
|
717
|
+
catch (err) {
|
|
718
|
+
res.status(500).json({ error: err.message });
|
|
719
|
+
}
|
|
720
|
+
});
|
|
721
|
+
// POST /api/switch-project — Switch to a different project folder
|
|
722
|
+
app.post("/api/switch-project", (req, res) => {
|
|
723
|
+
const { projectRoot } = req.body;
|
|
724
|
+
if (!projectRoot || typeof projectRoot !== "string") {
|
|
725
|
+
return res.status(400).json({ error: "projectRoot is required" });
|
|
726
|
+
}
|
|
727
|
+
const normalized = path.normalize(projectRoot);
|
|
728
|
+
if (!fs.existsSync(normalized) || !fs.statSync(normalized).isDirectory()) {
|
|
729
|
+
return res.status(400).json({ error: `Directory not found: ${normalized}` });
|
|
730
|
+
}
|
|
731
|
+
if (!switchProjectFn) {
|
|
732
|
+
return res.status(500).json({ error: "Switch project not supported" });
|
|
733
|
+
}
|
|
734
|
+
try {
|
|
735
|
+
const result = switchProjectFn(normalized);
|
|
736
|
+
broadcastSSE("project-switched", result);
|
|
737
|
+
res.json(result);
|
|
738
|
+
}
|
|
739
|
+
catch (err) {
|
|
740
|
+
res.status(500).json({ error: err.message || "Failed to switch project" });
|
|
741
|
+
}
|
|
742
|
+
});
|
|
743
|
+
return app;
|
|
744
|
+
}
|