@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.
Files changed (41) hide show
  1. package/README.md +112 -0
  2. package/dist/ai/analyzer.d.ts +3 -0
  3. package/dist/ai/analyzer.js +120 -0
  4. package/dist/ai/realtime-analyzer.d.ts +20 -0
  5. package/dist/ai/realtime-analyzer.js +182 -0
  6. package/dist/graph.d.ts +13 -0
  7. package/dist/graph.js +105 -0
  8. package/dist/index.d.ts +2 -0
  9. package/dist/index.js +518 -0
  10. package/dist/languages/cpp.d.ts +2 -0
  11. package/dist/languages/cpp.js +109 -0
  12. package/dist/languages/dart.d.ts +2 -0
  13. package/dist/languages/dart.js +162 -0
  14. package/dist/languages/go.d.ts +2 -0
  15. package/dist/languages/go.js +111 -0
  16. package/dist/languages/java.d.ts +2 -0
  17. package/dist/languages/java.js +113 -0
  18. package/dist/languages/plugin.d.ts +20 -0
  19. package/dist/languages/plugin.js +148 -0
  20. package/dist/languages/python.d.ts +2 -0
  21. package/dist/languages/python.js +129 -0
  22. package/dist/languages/ruby.d.ts +2 -0
  23. package/dist/languages/ruby.js +97 -0
  24. package/dist/languages/rust.d.ts +2 -0
  25. package/dist/languages/rust.js +121 -0
  26. package/dist/languages/typescript.d.ts +2 -0
  27. package/dist/languages/typescript.js +138 -0
  28. package/dist/license/validator.d.ts +23 -0
  29. package/dist/license/validator.js +297 -0
  30. package/dist/tools/analyze-impact.d.ts +23 -0
  31. package/dist/tools/analyze-impact.js +102 -0
  32. package/dist/tools/gate-build.d.ts +25 -0
  33. package/dist/tools/gate-build.js +243 -0
  34. package/dist/watcher/file-cache.d.ts +56 -0
  35. package/dist/watcher/file-cache.js +241 -0
  36. package/dist/web/public/app.js +2398 -0
  37. package/dist/web/public/index.html +258 -0
  38. package/dist/web/public/style.css +1827 -0
  39. package/dist/web/server.d.ts +29 -0
  40. package/dist/web/server.js +744 -0
  41. 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
+ }