sdl-mcp 0.12.2 → 0.12.3

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 (276) hide show
  1. package/README.md +10 -0
  2. package/config/sdlmcp.config.example.json +53 -8
  3. package/config/sdlmcp.config.schema.json +530 -106
  4. package/dist/.tsbuildinfo +1 -1
  5. package/dist/benchmark/external-manifest.d.ts +162 -0
  6. package/dist/benchmark/external-manifest.d.ts.map +1 -0
  7. package/dist/benchmark/external-manifest.js +336 -0
  8. package/dist/benchmark/external-manifest.js.map +1 -0
  9. package/dist/benchmark/external-runner.d.ts +100 -0
  10. package/dist/benchmark/external-runner.d.ts.map +1 -0
  11. package/dist/benchmark/external-runner.js +1171 -0
  12. package/dist/benchmark/external-runner.js.map +1 -0
  13. package/dist/benchmark/output-file.d.ts +3 -0
  14. package/dist/benchmark/output-file.d.ts.map +1 -0
  15. package/dist/benchmark/output-file.js +8 -0
  16. package/dist/benchmark/output-file.js.map +1 -0
  17. package/dist/cli/argParsing.d.ts.map +1 -1
  18. package/dist/cli/argParsing.js +6 -0
  19. package/dist/cli/argParsing.js.map +1 -1
  20. package/dist/cli/commands/benchmark.d.ts.map +1 -1
  21. package/dist/cli/commands/benchmark.js +3 -2
  22. package/dist/cli/commands/benchmark.js.map +1 -1
  23. package/dist/cli/commands/index.d.ts +1 -0
  24. package/dist/cli/commands/index.d.ts.map +1 -1
  25. package/dist/cli/commands/index.js +13 -7
  26. package/dist/cli/commands/index.js.map +1 -1
  27. package/dist/cli/commands/init.d.ts +5 -1
  28. package/dist/cli/commands/init.d.ts.map +1 -1
  29. package/dist/cli/commands/init.js +73 -3
  30. package/dist/cli/commands/init.js.map +1 -1
  31. package/dist/cli/commands/serve.d.ts.map +1 -1
  32. package/dist/cli/commands/serve.js +2 -0
  33. package/dist/cli/commands/serve.js.map +1 -1
  34. package/dist/cli/index.js +2 -0
  35. package/dist/cli/index.js.map +1 -1
  36. package/dist/cli/transport/http.d.ts +1 -35
  37. package/dist/cli/transport/http.d.ts.map +1 -1
  38. package/dist/cli/transport/http.js +151 -175
  39. package/dist/cli/transport/http.js.map +1 -1
  40. package/dist/cli/types.d.ts +1 -0
  41. package/dist/cli/types.d.ts.map +1 -1
  42. package/dist/config/constants.d.ts +30 -0
  43. package/dist/config/constants.d.ts.map +1 -1
  44. package/dist/config/constants.js +30 -0
  45. package/dist/config/constants.js.map +1 -1
  46. package/dist/config/types.d.ts +53 -1
  47. package/dist/config/types.d.ts.map +1 -1
  48. package/dist/config/types.js +62 -14
  49. package/dist/config/types.js.map +1 -1
  50. package/dist/db/ladybug-batching.d.ts +1 -0
  51. package/dist/db/ladybug-batching.d.ts.map +1 -1
  52. package/dist/db/ladybug-batching.js +1 -0
  53. package/dist/db/ladybug-batching.js.map +1 -1
  54. package/dist/db/ladybug-core.d.ts +3 -0
  55. package/dist/db/ladybug-core.d.ts.map +1 -1
  56. package/dist/db/ladybug-core.js +72 -2
  57. package/dist/db/ladybug-core.js.map +1 -1
  58. package/dist/db/ladybug-embeddings.d.ts +6 -1
  59. package/dist/db/ladybug-embeddings.d.ts.map +1 -1
  60. package/dist/db/ladybug-embeddings.js +6 -1
  61. package/dist/db/ladybug-embeddings.js.map +1 -1
  62. package/dist/db/ladybug-file-summaries.d.ts.map +1 -1
  63. package/dist/db/ladybug-file-summaries.js +23 -0
  64. package/dist/db/ladybug-file-summaries.js.map +1 -1
  65. package/dist/db/ladybug-queries.js +1 -1
  66. package/dist/db/ladybug-queries.js.map +1 -1
  67. package/dist/db/migrations/index.d.ts.map +1 -1
  68. package/dist/db/migrations/index.js +2 -0
  69. package/dist/db/migrations/index.js.map +1 -1
  70. package/dist/db/migrations/m007-copy-embeddings-to-symbol.d.ts +5 -9
  71. package/dist/db/migrations/m007-copy-embeddings-to-symbol.d.ts.map +1 -1
  72. package/dist/db/migrations/m007-copy-embeddings-to-symbol.js +24 -126
  73. package/dist/db/migrations/m007-copy-embeddings-to-symbol.js.map +1 -1
  74. package/dist/db/migrations/m021-remediate-symbol-embeddings.d.ts +5 -0
  75. package/dist/db/migrations/m021-remediate-symbol-embeddings.d.ts.map +1 -0
  76. package/dist/db/migrations/m021-remediate-symbol-embeddings.js +7 -0
  77. package/dist/db/migrations/m021-remediate-symbol-embeddings.js.map +1 -0
  78. package/dist/db/migrations/symbol-embedding-remediation.d.ts +57 -0
  79. package/dist/db/migrations/symbol-embedding-remediation.d.ts.map +1 -0
  80. package/dist/db/migrations/symbol-embedding-remediation.js +335 -0
  81. package/dist/db/migrations/symbol-embedding-remediation.js.map +1 -0
  82. package/dist/graph/layout/force-layout.d.ts +4 -0
  83. package/dist/graph/layout/force-layout.d.ts.map +1 -0
  84. package/dist/graph/layout/force-layout.js +127 -0
  85. package/dist/graph/layout/force-layout.js.map +1 -0
  86. package/dist/graph/layout/layout-service.d.ts +37 -0
  87. package/dist/graph/layout/layout-service.d.ts.map +1 -0
  88. package/dist/graph/layout/layout-service.js +172 -0
  89. package/dist/graph/layout/layout-service.js.map +1 -0
  90. package/dist/graph/layout/native-engine.d.ts +9 -0
  91. package/dist/graph/layout/native-engine.d.ts.map +1 -0
  92. package/dist/graph/layout/native-engine.js +59 -0
  93. package/dist/graph/layout/native-engine.js.map +1 -0
  94. package/dist/graph/layout/prng.d.ts +3 -0
  95. package/dist/graph/layout/prng.d.ts.map +1 -0
  96. package/dist/graph/layout/prng.js +19 -0
  97. package/dist/graph/layout/prng.js.map +1 -0
  98. package/dist/graph/layout/serializer.d.ts +3 -0
  99. package/dist/graph/layout/serializer.d.ts.map +1 -0
  100. package/dist/graph/layout/serializer.js +20 -0
  101. package/dist/graph/layout/serializer.js.map +1 -0
  102. package/dist/graph/layout/types.d.ts +30 -0
  103. package/dist/graph/layout/types.d.ts.map +1 -0
  104. package/dist/graph/layout/types.js +2 -0
  105. package/dist/graph/layout/types.js.map +1 -0
  106. package/dist/indexer/embeddings.d.ts +7 -0
  107. package/dist/indexer/embeddings.d.ts.map +1 -1
  108. package/dist/indexer/embeddings.js +44 -4
  109. package/dist/indexer/embeddings.js.map +1 -1
  110. package/dist/indexer/file-summary-embeddings.d.ts +12 -0
  111. package/dist/indexer/file-summary-embeddings.d.ts.map +1 -1
  112. package/dist/indexer/file-summary-embeddings.js +83 -21
  113. package/dist/indexer/file-summary-embeddings.js.map +1 -1
  114. package/dist/indexer/fileWalker.d.ts.map +1 -1
  115. package/dist/indexer/fileWalker.js +2 -1
  116. package/dist/indexer/fileWalker.js.map +1 -1
  117. package/dist/indexer/metrics-updater.d.ts.map +1 -1
  118. package/dist/indexer/metrics-updater.js +27 -4
  119. package/dist/indexer/metrics-updater.js.map +1 -1
  120. package/dist/indexer/watcher.d.ts.map +1 -1
  121. package/dist/indexer/watcher.js +2 -1
  122. package/dist/indexer/watcher.js.map +1 -1
  123. package/dist/mcp/context-response-projection.d.ts.map +1 -1
  124. package/dist/mcp/context-response-projection.js +49 -13
  125. package/dist/mcp/context-response-projection.js.map +1 -1
  126. package/dist/mcp/tools/search-edit/planner.d.ts.map +1 -1
  127. package/dist/mcp/tools/search-edit/planner.js +10 -3
  128. package/dist/mcp/tools/search-edit/planner.js.map +1 -1
  129. package/dist/observability/event-tap.d.ts +2 -1
  130. package/dist/observability/event-tap.d.ts.map +1 -1
  131. package/dist/observability/event-tap.js.map +1 -1
  132. package/dist/observability/service.d.ts +11 -4
  133. package/dist/observability/service.d.ts.map +1 -1
  134. package/dist/observability/service.js +83 -13
  135. package/dist/observability/service.js.map +1 -1
  136. package/dist/observability/types.d.ts +48 -2
  137. package/dist/observability/types.d.ts.map +1 -1
  138. package/dist/ui/.tsbuildinfo +1 -0
  139. package/dist/ui/vendor/fflate.js +2692 -0
  140. package/dist/ui/vendor/jsm/controls/ArcballControls.js +3543 -0
  141. package/dist/ui/vendor/jsm/controls/DragControls.js +452 -0
  142. package/dist/ui/vendor/jsm/controls/FirstPersonControls.js +488 -0
  143. package/dist/ui/vendor/jsm/controls/FlyControls.js +384 -0
  144. package/dist/ui/vendor/jsm/controls/MapControls.js +116 -0
  145. package/dist/ui/vendor/jsm/controls/OrbitControls.js +1963 -0
  146. package/dist/ui/vendor/jsm/controls/PointerLockControls.js +264 -0
  147. package/dist/ui/vendor/jsm/controls/TrackballControls.js +1001 -0
  148. package/dist/ui/vendor/jsm/controls/TransformControls.js +2003 -0
  149. package/dist/ui/vendor/jsm/loaders/3DMLoader.js +1836 -0
  150. package/dist/ui/vendor/jsm/loaders/3MFLoader.js +1621 -0
  151. package/dist/ui/vendor/jsm/loaders/AMFLoader.js +541 -0
  152. package/dist/ui/vendor/jsm/loaders/BVHLoader.js +484 -0
  153. package/dist/ui/vendor/jsm/loaders/ColladaLoader.js +153 -0
  154. package/dist/ui/vendor/jsm/loaders/DDSLoader.js +385 -0
  155. package/dist/ui/vendor/jsm/loaders/DRACOLoader.js +772 -0
  156. package/dist/ui/vendor/jsm/loaders/EXRLoader.js +3395 -0
  157. package/dist/ui/vendor/jsm/loaders/FBXLoader.js +4580 -0
  158. package/dist/ui/vendor/jsm/loaders/FontLoader.js +260 -0
  159. package/dist/ui/vendor/jsm/loaders/GCodeLoader.js +318 -0
  160. package/dist/ui/vendor/jsm/loaders/GLTFLoader.js +4860 -0
  161. package/dist/ui/vendor/jsm/loaders/HDRCubeTextureLoader.js +164 -0
  162. package/dist/ui/vendor/jsm/loaders/HDRLoader.js +463 -0
  163. package/dist/ui/vendor/jsm/loaders/IESLoader.js +379 -0
  164. package/dist/ui/vendor/jsm/loaders/KMZLoader.js +163 -0
  165. package/dist/ui/vendor/jsm/loaders/KTX2Loader.js +1276 -0
  166. package/dist/ui/vendor/jsm/loaders/KTXLoader.js +197 -0
  167. package/dist/ui/vendor/jsm/loaders/LDrawLoader.js +2510 -0
  168. package/dist/ui/vendor/jsm/loaders/LUT3dlLoader.js +205 -0
  169. package/dist/ui/vendor/jsm/loaders/LUTCubeLoader.js +190 -0
  170. package/dist/ui/vendor/jsm/loaders/LUTImageLoader.js +190 -0
  171. package/dist/ui/vendor/jsm/loaders/LWOLoader.js +1051 -0
  172. package/dist/ui/vendor/jsm/loaders/LottieLoader.js +130 -0
  173. package/dist/ui/vendor/jsm/loaders/MD2Loader.js +435 -0
  174. package/dist/ui/vendor/jsm/loaders/MDDLoader.js +147 -0
  175. package/dist/ui/vendor/jsm/loaders/MTLLoader.js +593 -0
  176. package/dist/ui/vendor/jsm/loaders/MaterialXLoader.js +1126 -0
  177. package/dist/ui/vendor/jsm/loaders/NRRDLoader.js +718 -0
  178. package/dist/ui/vendor/jsm/loaders/OBJLoader.js +955 -0
  179. package/dist/ui/vendor/jsm/loaders/PCDLoader.js +620 -0
  180. package/dist/ui/vendor/jsm/loaders/PDBLoader.js +272 -0
  181. package/dist/ui/vendor/jsm/loaders/PLYLoader.js +968 -0
  182. package/dist/ui/vendor/jsm/loaders/PVRLoader.js +270 -0
  183. package/dist/ui/vendor/jsm/loaders/RGBELoader.js +18 -0
  184. package/dist/ui/vendor/jsm/loaders/STLLoader.js +421 -0
  185. package/dist/ui/vendor/jsm/loaders/SVGLoader.js +3319 -0
  186. package/dist/ui/vendor/jsm/loaders/TDSLoader.js +1142 -0
  187. package/dist/ui/vendor/jsm/loaders/TGALoader.js +538 -0
  188. package/dist/ui/vendor/jsm/loaders/TIFFLoader.js +59 -0
  189. package/dist/ui/vendor/jsm/loaders/TTFLoader.js +261 -0
  190. package/dist/ui/vendor/jsm/loaders/USDLoader.js +303 -0
  191. package/dist/ui/vendor/jsm/loaders/USDZLoader.js +16 -0
  192. package/dist/ui/vendor/jsm/loaders/UltraHDRLoader.js +755 -0
  193. package/dist/ui/vendor/jsm/loaders/VOXLoader.js +919 -0
  194. package/dist/ui/vendor/jsm/loaders/VRMLLoader.js +3646 -0
  195. package/dist/ui/vendor/jsm/loaders/VTKLoader.js +1293 -0
  196. package/dist/ui/vendor/jsm/loaders/XYZLoader.js +143 -0
  197. package/dist/ui/vendor/jsm/loaders/collada/ColladaComposer.js +3044 -0
  198. package/dist/ui/vendor/jsm/loaders/collada/ColladaParser.js +1977 -0
  199. package/dist/ui/vendor/jsm/loaders/lwo/IFFParser.js +1217 -0
  200. package/dist/ui/vendor/jsm/loaders/lwo/LWO2Parser.js +414 -0
  201. package/dist/ui/vendor/jsm/loaders/lwo/LWO3Parser.js +373 -0
  202. package/dist/ui/vendor/jsm/loaders/usd/USDAParser.js +901 -0
  203. package/dist/ui/vendor/jsm/loaders/usd/USDCParser.js +1878 -0
  204. package/dist/ui/vendor/jsm/loaders/usd/USDComposer.js +4627 -0
  205. package/dist/ui/vendor/jsm/utils/BufferGeometryUtils.js +1501 -0
  206. package/dist/ui/vendor/jsm/utils/CameraUtils.js +82 -0
  207. package/dist/ui/vendor/jsm/utils/ColorUtils.js +76 -0
  208. package/dist/ui/vendor/jsm/utils/GeometryCompressionUtils.js +547 -0
  209. package/dist/ui/vendor/jsm/utils/GeometryUtils.js +226 -0
  210. package/dist/ui/vendor/jsm/utils/LDrawUtils.js +211 -0
  211. package/dist/ui/vendor/jsm/utils/SceneOptimizer.js +458 -0
  212. package/dist/ui/vendor/jsm/utils/SceneUtils.js +363 -0
  213. package/dist/ui/vendor/jsm/utils/ShadowMapViewer.js +244 -0
  214. package/dist/ui/vendor/jsm/utils/ShadowMapViewerGPU.js +233 -0
  215. package/dist/ui/vendor/jsm/utils/SkeletonUtils.js +496 -0
  216. package/dist/ui/vendor/jsm/utils/SortUtils.js +175 -0
  217. package/dist/ui/vendor/jsm/utils/UVsDebug.js +173 -0
  218. package/dist/ui/vendor/jsm/utils/WebGLTextureUtils.js +115 -0
  219. package/dist/ui/vendor/jsm/utils/WebGPUTextureUtils.js +81 -0
  220. package/dist/ui/vendor/jsm/utils/WorkerPool.js +167 -0
  221. package/dist/ui/vendor/three.core.min.js +6 -0
  222. package/dist/ui/vendor/three.module.min.js +6 -0
  223. package/dist/ui/viewer/ambient.js +39 -0
  224. package/dist/ui/viewer/api.js +93 -0
  225. package/dist/ui/viewer/chrome.js +36 -0
  226. package/dist/ui/viewer/edges.js +33 -0
  227. package/dist/ui/viewer/index.html +32 -0
  228. package/dist/ui/viewer/inspector.js +23 -0
  229. package/dist/ui/viewer/lenses/activity.js +24 -0
  230. package/dist/ui/viewer/lenses/community.js +5 -0
  231. package/dist/ui/viewer/lenses/edges.js +4 -0
  232. package/dist/ui/viewer/lenses/impact.js +6 -0
  233. package/dist/ui/viewer/lenses/search.js +69 -0
  234. package/dist/ui/viewer/lod.js +100 -0
  235. package/dist/ui/viewer/main.js +92 -0
  236. package/dist/ui/viewer/picking.js +45 -0
  237. package/dist/ui/viewer/scene.js +85 -0
  238. package/dist/ui/viewer/skins/default-skin.js +15 -0
  239. package/dist/ui/viewer/skins/loader.js +44 -0
  240. package/dist/ui/viewer/skins/manifest-schema.js +30 -0
  241. package/dist/ui/viewer/state.js +44 -0
  242. package/dist/ui/viewer/theme.js +23 -0
  243. package/dist/ui/viewer/universe.js +151 -0
  244. package/dist/ui/viewer/viewer.css +85 -0
  245. package/dist/util/safeRegex.d.ts.map +1 -1
  246. package/dist/util/safeRegex.js +221 -68
  247. package/dist/util/safeRegex.js.map +1 -1
  248. package/dist/viewer/legacy-graph.d.ts +40 -0
  249. package/dist/viewer/legacy-graph.d.ts.map +1 -0
  250. package/dist/viewer/legacy-graph.js +152 -0
  251. package/dist/viewer/legacy-graph.js.map +1 -0
  252. package/dist/viewer/routes.d.ts +10 -0
  253. package/dist/viewer/routes.d.ts.map +1 -0
  254. package/dist/viewer/routes.js +221 -0
  255. package/dist/viewer/routes.js.map +1 -0
  256. package/dist/viewer/service.d.ts +44 -0
  257. package/dist/viewer/service.d.ts.map +1 -0
  258. package/dist/viewer/service.js +160 -0
  259. package/dist/viewer/service.js.map +1 -0
  260. package/dist/viewer/types.d.ts +63 -0
  261. package/dist/viewer/types.d.ts.map +1 -0
  262. package/dist/viewer/types.js +2 -0
  263. package/dist/viewer/types.js.map +1 -0
  264. package/dist/viewer/viewer-config.d.ts +12 -0
  265. package/dist/viewer/viewer-config.d.ts.map +1 -0
  266. package/dist/viewer/viewer-config.js +36 -0
  267. package/dist/viewer/viewer-config.js.map +1 -0
  268. package/package.json +18 -9
  269. package/templates/SDL.md +19 -0
  270. package/templates/claude-code.json +8 -43
  271. package/dist/ui/graph.css +0 -108
  272. package/dist/ui/graph.d.ts +0 -2
  273. package/dist/ui/graph.d.ts.map +0 -1
  274. package/dist/ui/graph.html +0 -52
  275. package/dist/ui/graph.js +0 -259
  276. package/dist/ui/graph.js.map +0 -1
@@ -1,7 +1,7 @@
1
1
  import { createReadStream, existsSync, statSync } from "fs";
2
2
  import { createHash, randomBytes, randomUUID, timingSafeEqual } from "crypto";
3
3
  import { createServer } from "http";
4
- import { join } from "path";
4
+ import { extname, isAbsolute, join, relative, resolve } from "path";
5
5
  import { fileURLToPath } from "url";
6
6
  import { SSEServerTransport } from "@modelcontextprotocol/sdk/server/sse.js";
7
7
  import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
@@ -9,8 +9,6 @@ import { isInitializeRequest } from "@modelcontextprotocol/sdk/types.js";
9
9
  import { createMCPServer, } from "../../server.js";
10
10
  import { logger } from "../../util/logger.js";
11
11
  import * as ladybugDb from "../../db/ladybug-queries.js";
12
- import { computeDelta } from "../../delta/diff.js";
13
- import { runGovernorLoop } from "../../delta/blastRadius.js";
14
12
  import { getLadybugConn } from "../../db/ladybug.js";
15
13
  import { indexRepo } from "../../indexer/indexer.js";
16
14
  import { formatDeferredWorkStatus, getActiveDeferredWorkStatus, } from "../../runtime/deferred-work-state.js";
@@ -18,14 +16,16 @@ import { BufferCheckpointRequestSchema, BufferPushRequestSchema, } from "../../m
18
16
  import { handleSymbolGetCard, handleSymbolSearch, } from "../../mcp/tools/symbol.js";
19
17
  import { getDefaultLiveIndexCoordinator } from "../../live-index/coordinator.js";
20
18
  import { routeConfigAdminApiRequest } from "../../config/admin-api.js";
19
+ import { handleViewerApiRequest } from "../../viewer/routes.js";
20
+ import { buildBlastRadiusGraph, buildGraphForSliceHandle, buildNeighborhood } from "../../viewer/legacy-graph.js";
21
21
  import { SessionManager } from "../../mcp/session-manager.js";
22
22
  import { createRuntimeIdentity } from "../../util/runtime-identity.js";
23
23
  const __dirname = fileURLToPath(new URL(".", import.meta.url));
24
24
  const UI_DIR_CANDIDATE = join(__dirname, "..", "..", "ui");
25
- // Ensure graph.html actually exists in the candidate the compiled dist/ui
26
- // folder may contain only .js/.d.ts output from src/ui/graph.ts without the
27
- // html/css assets, in which case we must fall back to src/ui.
28
- const UI_DIR = existsSync(join(UI_DIR_CANDIDATE, "graph.html"))
25
+ // Ensure observability.html actually exists in the candidate. The compiled
26
+ // dist/ui folder may be missing copied html/css assets, in which case we must
27
+ // fall back to src/ui.
28
+ const UI_DIR = existsSync(join(UI_DIR_CANDIDATE, "observability.html"))
29
29
  ? UI_DIR_CANDIDATE
30
30
  : join(__dirname, "..", "..", "..", "src", "ui");
31
31
  /**
@@ -57,6 +57,7 @@ const OBSERVABILITY_UI_PATHS = new Set([
57
57
  "/ui/observability",
58
58
  "/ui/observability.js",
59
59
  "/ui/observability.css",
60
+ "/ui/viewer",
60
61
  ]);
61
62
  const LOCALHOST_ORIGIN_RE = /^https?:\/\/(localhost|127\.\d{1,3}\.\d{1,3}\.\d{1,3}|\[::1\]|\[::ffff:127\.\d{1,3}\.\d{1,3}\.\d{1,3}\])(:\d+)?$/;
62
63
  function isLoopbackBindHost(host) {
@@ -217,7 +218,7 @@ class InMemoryEventStore {
217
218
  async replayEventsAfter(lastEventId, { send, }) {
218
219
  const entry = this.events.get(lastEventId);
219
220
  if (!entry) {
220
- // Event was evicted by FIFO return an empty replay rather than
221
+ // Event was evicted by FIFO — return an empty replay rather than
221
222
  // throwing, which would crash the SDK's resumability path (M4).
222
223
  return "";
223
224
  }
@@ -379,165 +380,44 @@ export async function routeSymbolCardApiRequest(request, services = {}) {
379
380
  },
380
381
  };
381
382
  }
382
- // ---------------------------------------------------------------------------
383
- // Graph visualization helpers
384
- // ---------------------------------------------------------------------------
385
- function toClusterPath(filePath) {
386
- const slash = Math.max(filePath.lastIndexOf("/"), filePath.lastIndexOf("\\"));
387
- if (slash === -1) {
388
- return "root";
389
- }
390
- return filePath.slice(0, slash) || "root";
391
- }
392
- async function buildNodes(conn, symbolIds) {
393
- const symbolMap = await ladybugDb.getSymbolsByIds(conn, symbolIds);
394
- const metricsMap = await ladybugDb.getMetricsBySymbolIds(conn, symbolIds);
395
- const fileIds = new Set();
396
- for (const symbol of symbolMap.values()) {
397
- fileIds.add(symbol.fileId);
398
- }
399
- const fileMap = await ladybugDb.getFilesByIds(conn, Array.from(fileIds));
400
- const nodes = [];
401
- for (const symbolId of symbolIds) {
402
- const symbol = symbolMap.get(symbolId);
403
- if (!symbol)
404
- continue;
405
- const file = fileMap.get(symbol.fileId);
406
- const metrics = metricsMap.get(symbolId);
407
- nodes.push({
408
- id: symbolId,
409
- label: symbol.name,
410
- kind: symbol.kind,
411
- file: file?.relPath,
412
- fanIn: metrics?.fanIn,
413
- fanOut: metrics?.fanOut,
414
- size: Math.max(5, Math.min(40, (metrics?.fanIn ?? 0) + 6)),
415
- cluster: toClusterPath(file?.relPath ?? ""),
416
- });
417
- }
418
- return nodes;
419
- }
420
- async function buildLinksForNodes(conn, ids) {
421
- const idList = Array.from(ids);
422
- const edgeMap = await ladybugDb.getEdgesFromSymbolsForSlice(conn, idList);
423
- const links = [];
424
- for (const fromSymbolId of idList) {
425
- const edges = edgeMap.get(fromSymbolId) ?? [];
426
- for (const edge of edges) {
427
- if (!ids.has(edge.toSymbolId))
428
- continue;
429
- links.push({
430
- source: fromSymbolId,
431
- target: edge.toSymbolId,
432
- type: edge.edgeType,
433
- weight: edge.weight,
434
- });
435
- }
383
+ function contentTypeForUiPath(filePath) {
384
+ switch (extname(filePath)) {
385
+ case ".html":
386
+ return "text/html; charset=utf-8";
387
+ case ".css":
388
+ return "text/css; charset=utf-8";
389
+ case ".js":
390
+ return "application/javascript; charset=utf-8";
391
+ default:
392
+ return null;
436
393
  }
437
- return links;
438
- }
439
- function collapseClusters(nodes, maxChildrenPerCluster = 10) {
440
- const byCluster = new Map();
441
- for (const node of nodes) {
442
- const key = node.cluster ?? "root";
443
- const list = byCluster.get(key) ?? [];
444
- list.push(node);
445
- byCluster.set(key, list);
446
- }
447
- const collapsed = [];
448
- for (const [cluster, list] of byCluster) {
449
- if (list.length <= maxChildrenPerCluster) {
450
- collapsed.push(...list);
451
- continue;
452
- }
453
- const sorted = [...list].sort((a, b) => (b.fanIn ?? 0) - (a.fanIn ?? 0));
454
- collapsed.push(...sorted.slice(0, maxChildrenPerCluster));
455
- collapsed.push({
456
- id: `cluster:${cluster}`,
457
- label: `${cluster} (+${list.length - maxChildrenPerCluster})`,
458
- kind: "module",
459
- cluster,
460
- size: 10,
461
- });
462
- }
463
- return collapsed;
464
- }
465
- async function buildNeighborhood(conn, symbolId, maxNodes) {
466
- const ids = new Set();
467
- ids.add(symbolId);
468
- for (const edge of await ladybugDb.getEdgesFrom(conn, symbolId)) {
469
- ids.add(edge.toSymbolId);
470
- }
471
- const edgesTo = await ladybugDb.getEdgesToSymbols(conn, [symbolId]);
472
- for (const edge of edgesTo.get(symbolId) ?? []) {
473
- ids.add(edge.fromSymbolId);
474
- }
475
- const limited = new Set(Array.from(ids).slice(0, maxNodes));
476
- const nodes = await buildNodes(conn, Array.from(limited));
477
- const links = await buildLinksForNodes(conn, limited);
478
- return {
479
- nodes: collapseClusters(nodes),
480
- links,
481
- };
482
394
  }
483
- async function buildRepoPreview(conn, repoId, maxNodes) {
484
- const top = await ladybugDb.getTopSymbolsByFanIn(conn, repoId, maxNodes);
485
- const symbolIds = top.map((row) => row.symbolId);
486
- const ids = new Set(symbolIds);
487
- const nodes = await buildNodes(conn, symbolIds);
488
- const links = await buildLinksForNodes(conn, ids);
489
- return {
490
- nodes: collapseClusters(nodes),
491
- links,
492
- };
493
- }
494
- export async function buildGraphForSliceHandle(conn, repoId, handle, maxNodes, deps = {}) {
495
- const getSliceHandleFn = deps.getSliceHandle ?? ladybugDb.getSliceHandle;
496
- const buildRepoPreviewFn = deps.buildRepoPreview ?? buildRepoPreview;
497
- const buildBlastRadiusGraphFn = deps.buildBlastRadiusGraph ?? buildBlastRadiusGraph;
498
- const handleRow = await getSliceHandleFn(conn, handle);
499
- if (!handleRow || handleRow.repoId !== repoId) {
395
+ function getPrefixUiAsset(pathname) {
396
+ if (!pathname.startsWith("/ui/viewer/") && !pathname.startsWith("/ui/vendor/")) {
500
397
  return null;
501
398
  }
502
- if (handleRow.minVersion && handleRow.maxVersion) {
503
- return buildBlastRadiusGraphFn(conn, repoId, handleRow.minVersion, handleRow.maxVersion, maxNodes);
504
- }
505
- return buildRepoPreviewFn(conn, repoId, maxNodes);
399
+ const file = pathname.slice("/ui/".length);
400
+ const type = contentTypeForUiPath(file);
401
+ return type ? { file, type } : null;
506
402
  }
507
- async function buildBlastRadiusGraph(conn, repoId, fromVersion, toVersion, maxNodes) {
508
- const delta = await computeDelta(repoId, fromVersion, toVersion);
509
- const changedSymbolIds = delta.changedSymbols.map((change) => change.symbolId);
510
- const governor = await runGovernorLoop(conn, changedSymbolIds, {
511
- repoId,
512
- budget: { maxCards: maxNodes, maxEstimatedTokens: 4000 },
513
- runDiagnostics: false,
514
- });
515
- const ids = new Set();
516
- for (const changed of delta.changedSymbols) {
517
- ids.add(changed.symbolId);
518
- }
519
- for (const affected of governor.blastRadius) {
520
- ids.add(affected.symbolId);
403
+ function resolveUiAssetPath(file) {
404
+ const rootDir = resolve(UI_DIR);
405
+ const fullPath = resolve(rootDir, file);
406
+ const relativePath = relative(rootDir, fullPath);
407
+ if (relativePath.startsWith("..") || isAbsolute(relativePath)) {
408
+ return null;
521
409
  }
522
- const limited = new Set(Array.from(ids).slice(0, maxNodes));
523
- const nodes = await buildNodes(conn, Array.from(limited));
524
- const links = await buildLinksForNodes(conn, limited);
525
- return {
526
- nodes: collapseClusters(nodes),
527
- links,
528
- };
410
+ return fullPath;
529
411
  }
530
- // ---------------------------------------------------------------------------
531
- // Static UI assets
532
- // ---------------------------------------------------------------------------
533
412
  function serveUiAsset(pathname, res) {
413
+ if (pathname === "/ui/graph" || pathname === "/ui/graph.html") {
414
+ res.writeHead(308, { Location: "/ui/viewer" });
415
+ res.end();
416
+ return true;
417
+ }
534
418
  const map = {
535
- "/ui/graph": { file: "graph.html", type: "text/html; charset=utf-8" },
536
- "/ui/graph.js": {
537
- file: "graph.js",
538
- type: "application/javascript; charset=utf-8",
539
- },
540
- "/ui/graph.css": { file: "graph.css", type: "text/css; charset=utf-8" },
419
+ "/ui/viewer": { file: "viewer/index.html", type: "text/html; charset=utf-8" },
420
+ "/ui/viewer/index.html": { file: "viewer/index.html", type: "text/html; charset=utf-8" },
541
421
  "/ui/observability": {
542
422
  file: "observability.html",
543
423
  type: "text/html; charset=utf-8",
@@ -561,11 +441,15 @@ function serveUiAsset(pathname, res) {
561
441
  },
562
442
  "/ui/config.css": { file: "config.css", type: "text/css; charset=utf-8" },
563
443
  };
564
- const asset = map[pathname];
444
+ const asset = map[pathname] ?? getPrefixUiAsset(pathname);
565
445
  if (!asset) {
566
446
  return false;
567
447
  }
568
- const fullPath = join(UI_DIR, asset.file);
448
+ const fullPath = resolveUiAssetPath(asset.file);
449
+ if (!fullPath) {
450
+ json(res, 404, { error: "UI asset not found" });
451
+ return true;
452
+ }
569
453
  try {
570
454
  if (!statSync(fullPath).isFile()) {
571
455
  json(res, 500, { error: "Failed to read UI asset" });
@@ -599,15 +483,18 @@ function serveUiAsset(pathname, res) {
599
483
  res.destroy(error instanceof Error ? error : undefined);
600
484
  });
601
485
  stream.once("open", () => {
602
- res.writeHead(200, { "Content-Type": asset.type });
486
+ res.writeHead(200, { "Content-Type": asset.type, "Cache-Control": "no-cache" });
603
487
  stream.pipe(res);
604
488
  });
605
489
  return true;
606
490
  }
607
491
  // ---------------------------------------------------------------------------
608
492
  function serveObservabilityUiAsset(pathname, res) {
609
- if (!OBSERVABILITY_UI_PATHS.has(pathname))
493
+ if (!OBSERVABILITY_UI_PATHS.has(pathname) &&
494
+ !pathname.startsWith("/ui/viewer/") &&
495
+ !pathname.startsWith("/ui/vendor/")) {
610
496
  return false;
497
+ }
611
498
  return serveUiAsset(pathname, res);
612
499
  }
613
500
  async function handleHealthRequest(req, res, checkHealth) {
@@ -695,6 +582,87 @@ async function routeObservabilityApiRequest(req, res, url, services) {
695
582
  json(res, 503, { error: "observability_disabled" });
696
583
  return true;
697
584
  }
585
+ if ((url.searchParams.get("types") ?? "").trim() === "graph") {
586
+ setCorsHeaders(req, res);
587
+ res.writeHead(200, {
588
+ "Content-Type": "text/event-stream",
589
+ "Cache-Control": "no-cache, no-transform",
590
+ Connection: "keep-alive",
591
+ "X-Accel-Buffering": "no",
592
+ });
593
+ let closed = false;
594
+ let unsubscribe = null;
595
+ let heartbeatTimer = null;
596
+ let maxStreamTimer = null;
597
+ const sendGraphEvent = (data) => {
598
+ if (closed || res.destroyed)
599
+ return false;
600
+ try {
601
+ const ok = res.write(`event: graph\ndata: ${JSON.stringify(data)}\n\n`);
602
+ return ok && res.writableLength <= 1_048_576;
603
+ }
604
+ catch (err) {
605
+ logger.warn("Graph SSE write failed", { error: err });
606
+ return false;
607
+ }
608
+ };
609
+ const cleanupGraph = () => {
610
+ if (closed)
611
+ return;
612
+ closed = true;
613
+ if (heartbeatTimer) {
614
+ clearInterval(heartbeatTimer);
615
+ heartbeatTimer = null;
616
+ }
617
+ if (maxStreamTimer) {
618
+ clearTimeout(maxStreamTimer);
619
+ maxStreamTimer = null;
620
+ }
621
+ if (unsubscribe) {
622
+ unsubscribe();
623
+ unsubscribe = null;
624
+ }
625
+ };
626
+ const endGraphStream = () => {
627
+ cleanupGraph();
628
+ try {
629
+ res.end();
630
+ }
631
+ catch {
632
+ /* best-effort */
633
+ }
634
+ };
635
+ req.on("close", cleanupGraph);
636
+ for (const event of observabilityService.getRecentGraphEvents(200)) {
637
+ if (!sendGraphEvent(event)) {
638
+ endGraphStream();
639
+ return true;
640
+ }
641
+ }
642
+ unsubscribe = observabilityService.onGraphEvent((event) => {
643
+ if (!sendGraphEvent(event)) {
644
+ endGraphStream();
645
+ }
646
+ });
647
+ heartbeatTimer = setInterval(() => {
648
+ if (closed || res.destroyed) {
649
+ cleanupGraph();
650
+ return;
651
+ }
652
+ try {
653
+ res.write(": heartbeat\n\n");
654
+ }
655
+ catch {
656
+ endGraphStream();
657
+ }
658
+ }, sseHeartbeatMs);
659
+ heartbeatTimer.unref();
660
+ maxStreamTimer = setTimeout(() => {
661
+ endGraphStream();
662
+ }, sseMaxStreamMs);
663
+ maxStreamTimer.unref();
664
+ return true;
665
+ }
698
666
  const repoId = (url.searchParams.get("repoId") ?? "").trim();
699
667
  if (!repoId) {
700
668
  json(res, 400, { error: "missing_repoId" });
@@ -821,6 +789,10 @@ export async function handleObservabilityDashboardRequest(req, res, host, port,
821
789
  return true;
822
790
  }
823
791
  try {
792
+ if (pathname.startsWith("/api/graph/") &&
793
+ (await handleViewerApiRequest(req, res, pathname, url, services.observabilityService ?? null))) {
794
+ return true;
795
+ }
824
796
  return await routeObservabilityApiRequest(req, res, url, services);
825
797
  }
826
798
  catch (error) {
@@ -899,9 +871,12 @@ async function handleRestRequest(req, res, host, port, checkHealth, services) {
899
871
  return true;
900
872
  }
901
873
  }
902
- // DB-dependent REST API routes wrapped in try/catch for structured errors.
874
+ // DB-dependent REST API routes — wrapped in try/catch for structured errors.
903
875
  // Connection is acquired lazily per-route to avoid unnecessary pool checkouts.
904
876
  try {
877
+ if (await handleViewerApiRequest(req, res, pathname, url, services.observabilityService)) {
878
+ return true;
879
+ }
905
880
  const graphSliceMatch = pathname.match(/^\/api\/graph\/([^/]+)\/slice\/([^/]+)$/);
906
881
  if (req.method === "GET" && graphSliceMatch) {
907
882
  const conn = await getLadybugConn();
@@ -1030,7 +1005,7 @@ async function handleRestRequest(req, res, host, port, checkHealth, services) {
1030
1005
  });
1031
1006
  return true;
1032
1007
  }
1033
- // SSE streaming reindex endpoint streams progress events, then a
1008
+ // SSE streaming reindex endpoint — streams progress events, then a
1034
1009
  // final "complete" or "error" event. Used by `sdl-mcp index` when it
1035
1010
  // detects a running HTTP server and delegates indexing.
1036
1011
  const reindexStreamMatch = pathname.match(/^\/api\/repo\/([^/]+)\/reindex-stream$/);
@@ -1049,7 +1024,7 @@ async function handleRestRequest(req, res, host, port, checkHealth, services) {
1049
1024
  }
1050
1025
  }
1051
1026
  catch {
1052
- // Empty or invalid body use default mode
1027
+ // Empty or invalid body — use default mode
1053
1028
  }
1054
1029
  // Set up SSE response
1055
1030
  setCorsHeaders(req, res);
@@ -1234,7 +1209,7 @@ async function handleMcpStreamableRequest(req, res, ctx) {
1234
1209
  ctx.cleanupSession(registeredSessionId);
1235
1210
  }
1236
1211
  else {
1237
- // MCPServer was created but never connected stop it directly
1212
+ // MCPServer was created but never connected — stop it directly
1238
1213
  void mcpServer.stop().catch((err) => {
1239
1214
  logger.debug("Failed to stop unconnected MCP server", {
1240
1215
  error: err,
@@ -1305,7 +1280,7 @@ async function handleMcpStreamableRequest(req, res, ctx) {
1305
1280
  }
1306
1281
  }
1307
1282
  /**
1308
- * Handle `GET /sse` establishes a new SSE (deprecated) transport session.
1283
+ * Handle `GET /sse` — establishes a new SSE (deprecated) transport session.
1309
1284
  * Validates the `Accept: text/event-stream` header, reserves a session slot,
1310
1285
  * creates the SSEServerTransport, and connects a new per-session MCP server.
1311
1286
  */
@@ -1364,7 +1339,7 @@ async function handleSseConnection(req, res, ctx) {
1364
1339
  }
1365
1340
  }
1366
1341
  /**
1367
- * Handle `POST /message` routes an incoming MCP message to an existing SSE
1342
+ * Handle `POST /message` — routes an incoming MCP message to an existing SSE
1368
1343
  * (deprecated) session identified by the `sessionId` query parameter.
1369
1344
  */
1370
1345
  function handleSseMessage(req, res, url, ctx) {
@@ -1436,7 +1411,8 @@ export async function setupObservabilityDashboardSidecar(port, services = {}, ht
1436
1411
  const url = new URL(req.url ?? "/", `http://${host}:${port}`);
1437
1412
  const pathname = url.pathname;
1438
1413
  if (req.method !== "OPTIONS" &&
1439
- pathname.startsWith("/api/observability/") &&
1414
+ (pathname.startsWith("/api/observability/") ||
1415
+ pathname.startsWith("/api/graph/")) &&
1440
1416
  authToken &&
1441
1417
  !isAuthorized(req, authToken)) {
1442
1418
  const rateLimit = authRateLimiter.consume(getAuthRateLimitKey(req));
@@ -1536,7 +1512,7 @@ export async function setupHttpTransport(host, port, _graphDbPath, services = {}
1536
1512
  const cleanedUp = new Set();
1537
1513
  const sessionManager = services.sessionManager ?? new SessionManager(8);
1538
1514
  /**
1539
- * Idempotent session cleanup safe to call from onclose, idle reaper,
1515
+ * Idempotent session cleanup — safe to call from onclose, idle reaper,
1540
1516
  * error handlers, or graceful shutdown. Cleans transport, MCPServer, and
1541
1517
  * session manager state exactly once per session.
1542
1518
  */
@@ -1584,7 +1560,7 @@ export async function setupHttpTransport(host, port, _graphDbPath, services = {}
1584
1560
  effectiveServices,
1585
1561
  cleanupSession,
1586
1562
  };
1587
- // DB is already initialized by serve.ts initGraphDb() before this
1563
+ // DB is already initialized by serve.ts → initGraphDb() before this
1588
1564
  // function is called. A redundant initLadybugDb() here risked a
1589
1565
  // close+reopen cycle when normalizeGraphDbPath() returned a different
1590
1566
  // path (the DB directory now exists, flipping the hint from "file" to
@@ -1616,7 +1592,7 @@ export async function setupHttpTransport(host, port, _graphDbPath, services = {}
1616
1592
  (pathname === "/.well-known/oauth-authorization-server" ||
1617
1593
  pathname === "/.well-known/oauth-protected-resource")) {
1618
1594
  json(res, 404, {
1619
- error: "OAuth discovery not supported this server does not require authentication",
1595
+ error: "OAuth discovery not supported — this server does not require authentication",
1620
1596
  });
1621
1597
  return;
1622
1598
  }
@@ -1666,14 +1642,14 @@ export async function setupHttpTransport(host, port, _graphDbPath, services = {}
1666
1642
  return;
1667
1643
  }
1668
1644
  // ---------------------------------------------------------------
1669
- // Streamable HTTP Transport /mcp (POST, GET, DELETE)
1645
+ // Streamable HTTP Transport — /mcp (POST, GET, DELETE)
1670
1646
  // ---------------------------------------------------------------
1671
1647
  if (pathname === "/mcp") {
1672
1648
  await handleMcpStreamableRequest(req, res, sessionCtx);
1673
1649
  return;
1674
1650
  }
1675
1651
  // ---------------------------------------------------------------
1676
- // Deprecated SSE Transport /sse (GET) + /message (POST)
1652
+ // Deprecated SSE Transport — /sse (GET) + /message (POST)
1677
1653
  // Supports multiple concurrent SSE sessions (no singleton)
1678
1654
  // ---------------------------------------------------------------
1679
1655
  if (req.method === "GET" && pathname === "/sse") {
@@ -1734,7 +1710,7 @@ export async function setupHttpTransport(host, port, _graphDbPath, services = {}
1734
1710
  console.error(` - SSE endpoint: http://${host}:${actualPort}/sse`);
1735
1711
  console.error(` - Sessions: http://${host}:${actualPort}/api/sessions`);
1736
1712
  console.error(` - Health check: http://${host}:${actualPort}/health`);
1737
- console.error(` - Graph UI: http://${host}:${actualPort}/ui/graph`);
1713
+ console.error(` - Graph UI: http://${host}:${actualPort}/ui/viewer`);
1738
1714
  if (host !== "localhost" && host !== "127.0.0.1" && host !== "::1") {
1739
1715
  console.error(`\n WARNING: Server is listening on ${host} over plaintext HTTP.` +
1740
1716
  `\n Traffic (including code content) is NOT encrypted.` +