@decibelsystems/tools 2.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 (478) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +342 -0
  3. package/dist/agentic/compiler.d.ts +21 -0
  4. package/dist/agentic/compiler.d.ts.map +1 -0
  5. package/dist/agentic/compiler.js +267 -0
  6. package/dist/agentic/compiler.js.map +1 -0
  7. package/dist/agentic/golden.d.ts +25 -0
  8. package/dist/agentic/golden.d.ts.map +1 -0
  9. package/dist/agentic/golden.js +255 -0
  10. package/dist/agentic/golden.js.map +1 -0
  11. package/dist/agentic/index.d.ts +17 -0
  12. package/dist/agentic/index.d.ts.map +1 -0
  13. package/dist/agentic/index.js +153 -0
  14. package/dist/agentic/index.js.map +1 -0
  15. package/dist/agentic/linter.d.ts +20 -0
  16. package/dist/agentic/linter.d.ts.map +1 -0
  17. package/dist/agentic/linter.js +340 -0
  18. package/dist/agentic/linter.js.map +1 -0
  19. package/dist/agentic/renderer.d.ts +17 -0
  20. package/dist/agentic/renderer.d.ts.map +1 -0
  21. package/dist/agentic/renderer.js +277 -0
  22. package/dist/agentic/renderer.js.map +1 -0
  23. package/dist/agentic/types.d.ts +199 -0
  24. package/dist/agentic/types.d.ts.map +1 -0
  25. package/dist/agentic/types.js +8 -0
  26. package/dist/agentic/types.js.map +1 -0
  27. package/dist/architectAdrs.d.ts +32 -0
  28. package/dist/architectAdrs.d.ts.map +1 -0
  29. package/dist/architectAdrs.js +162 -0
  30. package/dist/architectAdrs.js.map +1 -0
  31. package/dist/client/facade-client.d.ts +41 -0
  32. package/dist/client/facade-client.d.ts.map +1 -0
  33. package/dist/client/facade-client.js +243 -0
  34. package/dist/client/facade-client.js.map +1 -0
  35. package/dist/client/index.d.ts +4 -0
  36. package/dist/client/index.d.ts.map +1 -0
  37. package/dist/client/index.js +18 -0
  38. package/dist/client/index.js.map +1 -0
  39. package/dist/client/transports.d.ts +78 -0
  40. package/dist/client/transports.d.ts.map +1 -0
  41. package/dist/client/transports.js +258 -0
  42. package/dist/client/transports.js.map +1 -0
  43. package/dist/client/types.d.ts +49 -0
  44. package/dist/client/types.d.ts.map +1 -0
  45. package/dist/client/types.js +8 -0
  46. package/dist/client/types.js.map +1 -0
  47. package/dist/config.d.ts +8 -0
  48. package/dist/config.d.ts.map +1 -0
  49. package/dist/config.js +19 -0
  50. package/dist/config.js.map +1 -0
  51. package/dist/daemon.d.ts +77 -0
  52. package/dist/daemon.d.ts.map +1 -0
  53. package/dist/daemon.js +374 -0
  54. package/dist/daemon.js.map +1 -0
  55. package/dist/daemonConfig.d.ts +43 -0
  56. package/dist/daemonConfig.d.ts.map +1 -0
  57. package/dist/daemonConfig.js +113 -0
  58. package/dist/daemonConfig.js.map +1 -0
  59. package/dist/dataRoot.d.ts +5 -0
  60. package/dist/dataRoot.d.ts.map +1 -0
  61. package/dist/dataRoot.js +23 -0
  62. package/dist/dataRoot.js.map +1 -0
  63. package/dist/decibelPaths.d.ts +42 -0
  64. package/dist/decibelPaths.d.ts.map +1 -0
  65. package/dist/decibelPaths.js +150 -0
  66. package/dist/decibelPaths.js.map +1 -0
  67. package/dist/facades/definitions.d.ts +6 -0
  68. package/dist/facades/definitions.d.ts.map +1 -0
  69. package/dist/facades/definitions.js +450 -0
  70. package/dist/facades/definitions.js.map +1 -0
  71. package/dist/facades/index.d.ts +27 -0
  72. package/dist/facades/index.d.ts.map +1 -0
  73. package/dist/facades/index.js +124 -0
  74. package/dist/facades/index.js.map +1 -0
  75. package/dist/facades/types.d.ts +38 -0
  76. package/dist/facades/types.d.ts.map +1 -0
  77. package/dist/facades/types.js +8 -0
  78. package/dist/facades/types.js.map +1 -0
  79. package/dist/httpServer.d.ts +66 -0
  80. package/dist/httpServer.d.ts.map +1 -0
  81. package/dist/httpServer.js +1723 -0
  82. package/dist/httpServer.js.map +1 -0
  83. package/dist/kernel.d.ts +87 -0
  84. package/dist/kernel.d.ts.map +1 -0
  85. package/dist/kernel.js +256 -0
  86. package/dist/kernel.js.map +1 -0
  87. package/dist/lib/agent-services/assumptions.d.ts +16 -0
  88. package/dist/lib/agent-services/assumptions.d.ts.map +1 -0
  89. package/dist/lib/agent-services/assumptions.js +284 -0
  90. package/dist/lib/agent-services/assumptions.js.map +1 -0
  91. package/dist/lib/agent-services/context-pack.d.ts +6 -0
  92. package/dist/lib/agent-services/context-pack.d.ts.map +1 -0
  93. package/dist/lib/agent-services/context-pack.js +354 -0
  94. package/dist/lib/agent-services/context-pack.js.map +1 -0
  95. package/dist/lib/agent-services/drift-guard.d.ts +14 -0
  96. package/dist/lib/agent-services/drift-guard.d.ts.map +1 -0
  97. package/dist/lib/agent-services/drift-guard.js +355 -0
  98. package/dist/lib/agent-services/drift-guard.js.map +1 -0
  99. package/dist/lib/agent-services/index.d.ts +5 -0
  100. package/dist/lib/agent-services/index.d.ts.map +1 -0
  101. package/dist/lib/agent-services/index.js +10 -0
  102. package/dist/lib/agent-services/index.js.map +1 -0
  103. package/dist/lib/benchmark.d.ts +110 -0
  104. package/dist/lib/benchmark.d.ts.map +1 -0
  105. package/dist/lib/benchmark.js +338 -0
  106. package/dist/lib/benchmark.js.map +1 -0
  107. package/dist/lib/supabase.d.ts +123 -0
  108. package/dist/lib/supabase.d.ts.map +1 -0
  109. package/dist/lib/supabase.js +91 -0
  110. package/dist/lib/supabase.js.map +1 -0
  111. package/dist/license.d.ts +30 -0
  112. package/dist/license.d.ts.map +1 -0
  113. package/dist/license.js +131 -0
  114. package/dist/license.js.map +1 -0
  115. package/dist/projectPaths.d.ts +27 -0
  116. package/dist/projectPaths.d.ts.map +1 -0
  117. package/dist/projectPaths.js +86 -0
  118. package/dist/projectPaths.js.map +1 -0
  119. package/dist/projectRegistry.d.ts +97 -0
  120. package/dist/projectRegistry.d.ts.map +1 -0
  121. package/dist/projectRegistry.js +374 -0
  122. package/dist/projectRegistry.js.map +1 -0
  123. package/dist/sentinelIssues.d.ts +65 -0
  124. package/dist/sentinelIssues.d.ts.map +1 -0
  125. package/dist/sentinelIssues.js +297 -0
  126. package/dist/sentinelIssues.js.map +1 -0
  127. package/dist/server.d.ts +3 -0
  128. package/dist/server.d.ts.map +1 -0
  129. package/dist/server.js +195 -0
  130. package/dist/server.js.map +1 -0
  131. package/dist/test.d.ts +7 -0
  132. package/dist/test.d.ts.map +1 -0
  133. package/dist/test.js +77 -0
  134. package/dist/test.js.map +1 -0
  135. package/dist/tools/agentic/index.d.ts +7 -0
  136. package/dist/tools/agentic/index.d.ts.map +1 -0
  137. package/dist/tools/agentic/index.js +203 -0
  138. package/dist/tools/agentic/index.js.map +1 -0
  139. package/dist/tools/architect/index.d.ts +11 -0
  140. package/dist/tools/architect/index.d.ts.map +1 -0
  141. package/dist/tools/architect/index.js +506 -0
  142. package/dist/tools/architect/index.js.map +1 -0
  143. package/dist/tools/architect.d.ts +19 -0
  144. package/dist/tools/architect.d.ts.map +1 -0
  145. package/dist/tools/architect.js +88 -0
  146. package/dist/tools/architect.js.map +1 -0
  147. package/dist/tools/auditor/index.d.ts +10 -0
  148. package/dist/tools/auditor/index.d.ts.map +1 -0
  149. package/dist/tools/auditor/index.js +310 -0
  150. package/dist/tools/auditor/index.js.map +1 -0
  151. package/dist/tools/auditor.d.ts +149 -0
  152. package/dist/tools/auditor.d.ts.map +1 -0
  153. package/dist/tools/auditor.js +775 -0
  154. package/dist/tools/auditor.js.map +1 -0
  155. package/dist/tools/bench/index.d.ts +3 -0
  156. package/dist/tools/bench/index.d.ts.map +1 -0
  157. package/dist/tools/bench/index.js +220 -0
  158. package/dist/tools/bench/index.js.map +1 -0
  159. package/dist/tools/bench.d.ts +89 -0
  160. package/dist/tools/bench.d.ts.map +1 -0
  161. package/dist/tools/bench.js +826 -0
  162. package/dist/tools/bench.js.map +1 -0
  163. package/dist/tools/context/index.d.ts +11 -0
  164. package/dist/tools/context/index.d.ts.map +1 -0
  165. package/dist/tools/context/index.js +482 -0
  166. package/dist/tools/context/index.js.map +1 -0
  167. package/dist/tools/context.d.ts +146 -0
  168. package/dist/tools/context.d.ts.map +1 -0
  169. package/dist/tools/context.js +481 -0
  170. package/dist/tools/context.js.map +1 -0
  171. package/dist/tools/coordinator/coordinator.d.ts +168 -0
  172. package/dist/tools/coordinator/coordinator.d.ts.map +1 -0
  173. package/dist/tools/coordinator/coordinator.js +535 -0
  174. package/dist/tools/coordinator/coordinator.js.map +1 -0
  175. package/dist/tools/coordinator/index.d.ts +12 -0
  176. package/dist/tools/coordinator/index.d.ts.map +1 -0
  177. package/dist/tools/coordinator/index.js +381 -0
  178. package/dist/tools/coordinator/index.js.map +1 -0
  179. package/dist/tools/corpus/index.d.ts +5 -0
  180. package/dist/tools/corpus/index.d.ts.map +1 -0
  181. package/dist/tools/corpus/index.js +105 -0
  182. package/dist/tools/corpus/index.js.map +1 -0
  183. package/dist/tools/corpus.d.ts +33 -0
  184. package/dist/tools/corpus.d.ts.map +1 -0
  185. package/dist/tools/corpus.js +180 -0
  186. package/dist/tools/corpus.js.map +1 -0
  187. package/dist/tools/crit.d.ts +63 -0
  188. package/dist/tools/crit.d.ts.map +1 -0
  189. package/dist/tools/crit.js +159 -0
  190. package/dist/tools/crit.js.map +1 -0
  191. package/dist/tools/data-inspector.d.ts +189 -0
  192. package/dist/tools/data-inspector.d.ts.map +1 -0
  193. package/dist/tools/data-inspector.js +669 -0
  194. package/dist/tools/data-inspector.js.map +1 -0
  195. package/dist/tools/deck.d.ts +11 -0
  196. package/dist/tools/deck.d.ts.map +1 -0
  197. package/dist/tools/deck.js +188 -0
  198. package/dist/tools/deck.js.map +1 -0
  199. package/dist/tools/designer/index.d.ts +11 -0
  200. package/dist/tools/designer/index.d.ts.map +1 -0
  201. package/dist/tools/designer/index.js +442 -0
  202. package/dist/tools/designer/index.js.map +1 -0
  203. package/dist/tools/designer/lateral-tools.d.ts +6 -0
  204. package/dist/tools/designer/lateral-tools.d.ts.map +1 -0
  205. package/dist/tools/designer/lateral-tools.js +190 -0
  206. package/dist/tools/designer/lateral-tools.js.map +1 -0
  207. package/dist/tools/designer.d.ts +122 -0
  208. package/dist/tools/designer.d.ts.map +1 -0
  209. package/dist/tools/designer.js +495 -0
  210. package/dist/tools/designer.js.map +1 -0
  211. package/dist/tools/dojo/index.d.ts +13 -0
  212. package/dist/tools/dojo/index.d.ts.map +1 -0
  213. package/dist/tools/dojo/index.js +613 -0
  214. package/dist/tools/dojo/index.js.map +1 -0
  215. package/dist/tools/dojo.d.ts +254 -0
  216. package/dist/tools/dojo.d.ts.map +1 -0
  217. package/dist/tools/dojo.js +933 -0
  218. package/dist/tools/dojo.js.map +1 -0
  219. package/dist/tools/dojoBench.d.ts +49 -0
  220. package/dist/tools/dojoBench.d.ts.map +1 -0
  221. package/dist/tools/dojoBench.js +205 -0
  222. package/dist/tools/dojoBench.js.map +1 -0
  223. package/dist/tools/dojoGraduated.d.ts +50 -0
  224. package/dist/tools/dojoGraduated.d.ts.map +1 -0
  225. package/dist/tools/dojoGraduated.js +174 -0
  226. package/dist/tools/dojoGraduated.js.map +1 -0
  227. package/dist/tools/dojoPolicy.d.ts +65 -0
  228. package/dist/tools/dojoPolicy.d.ts.map +1 -0
  229. package/dist/tools/dojoPolicy.js +263 -0
  230. package/dist/tools/dojoPolicy.js.map +1 -0
  231. package/dist/tools/feedback/index.d.ts +5 -0
  232. package/dist/tools/feedback/index.d.ts.map +1 -0
  233. package/dist/tools/feedback/index.js +153 -0
  234. package/dist/tools/feedback/index.js.map +1 -0
  235. package/dist/tools/feedback.d.ts +61 -0
  236. package/dist/tools/feedback.d.ts.map +1 -0
  237. package/dist/tools/feedback.js +209 -0
  238. package/dist/tools/feedback.js.map +1 -0
  239. package/dist/tools/forecast/index.d.ts +8 -0
  240. package/dist/tools/forecast/index.d.ts.map +1 -0
  241. package/dist/tools/forecast/index.js +283 -0
  242. package/dist/tools/forecast/index.js.map +1 -0
  243. package/dist/tools/forecast.d.ts +147 -0
  244. package/dist/tools/forecast.d.ts.map +1 -0
  245. package/dist/tools/forecast.js +417 -0
  246. package/dist/tools/forecast.js.map +1 -0
  247. package/dist/tools/friction/index.d.ts +7 -0
  248. package/dist/tools/friction/index.d.ts.map +1 -0
  249. package/dist/tools/friction/index.js +265 -0
  250. package/dist/tools/friction/index.js.map +1 -0
  251. package/dist/tools/friction.d.ts +82 -0
  252. package/dist/tools/friction.d.ts.map +1 -0
  253. package/dist/tools/friction.js +331 -0
  254. package/dist/tools/friction.js.map +1 -0
  255. package/dist/tools/git/index.d.ts +9 -0
  256. package/dist/tools/git/index.d.ts.map +1 -0
  257. package/dist/tools/git/index.js +237 -0
  258. package/dist/tools/git/index.js.map +1 -0
  259. package/dist/tools/git-sentinel/index.d.ts +7 -0
  260. package/dist/tools/git-sentinel/index.d.ts.map +1 -0
  261. package/dist/tools/git-sentinel/index.js +178 -0
  262. package/dist/tools/git-sentinel/index.js.map +1 -0
  263. package/dist/tools/git-sentinel.d.ts +78 -0
  264. package/dist/tools/git-sentinel.d.ts.map +1 -0
  265. package/dist/tools/git-sentinel.js +391 -0
  266. package/dist/tools/git-sentinel.js.map +1 -0
  267. package/dist/tools/git.d.ts +134 -0
  268. package/dist/tools/git.d.ts.map +1 -0
  269. package/dist/tools/git.js +374 -0
  270. package/dist/tools/git.js.map +1 -0
  271. package/dist/tools/guardian/index.d.ts +8 -0
  272. package/dist/tools/guardian/index.d.ts.map +1 -0
  273. package/dist/tools/guardian/index.js +171 -0
  274. package/dist/tools/guardian/index.js.map +1 -0
  275. package/dist/tools/guardian.d.ts +62 -0
  276. package/dist/tools/guardian.d.ts.map +1 -0
  277. package/dist/tools/guardian.js +332 -0
  278. package/dist/tools/guardian.js.map +1 -0
  279. package/dist/tools/hygiene/codebase-scanner.d.ts +38 -0
  280. package/dist/tools/hygiene/codebase-scanner.d.ts.map +1 -0
  281. package/dist/tools/hygiene/codebase-scanner.js +411 -0
  282. package/dist/tools/hygiene/codebase-scanner.js.map +1 -0
  283. package/dist/tools/hygiene/config-scanner.d.ts +33 -0
  284. package/dist/tools/hygiene/config-scanner.d.ts.map +1 -0
  285. package/dist/tools/hygiene/config-scanner.js +482 -0
  286. package/dist/tools/hygiene/config-scanner.js.map +1 -0
  287. package/dist/tools/hygiene/coverage-scanner.d.ts +41 -0
  288. package/dist/tools/hygiene/coverage-scanner.d.ts.map +1 -0
  289. package/dist/tools/hygiene/coverage-scanner.js +331 -0
  290. package/dist/tools/hygiene/coverage-scanner.js.map +1 -0
  291. package/dist/tools/hygiene/index.d.ts +7 -0
  292. package/dist/tools/hygiene/index.d.ts.map +1 -0
  293. package/dist/tools/hygiene/index.js +291 -0
  294. package/dist/tools/hygiene/index.js.map +1 -0
  295. package/dist/tools/hygiene/oracle-hygiene.d.ts +68 -0
  296. package/dist/tools/hygiene/oracle-hygiene.d.ts.map +1 -0
  297. package/dist/tools/hygiene/oracle-hygiene.js +324 -0
  298. package/dist/tools/hygiene/oracle-hygiene.js.map +1 -0
  299. package/dist/tools/index.d.ts +6 -0
  300. package/dist/tools/index.d.ts.map +1 -0
  301. package/dist/tools/index.js +130 -0
  302. package/dist/tools/index.js.map +1 -0
  303. package/dist/tools/lateral.d.ts +114 -0
  304. package/dist/tools/lateral.d.ts.map +1 -0
  305. package/dist/tools/lateral.js +536 -0
  306. package/dist/tools/lateral.js.map +1 -0
  307. package/dist/tools/learnings/index.d.ts +5 -0
  308. package/dist/tools/learnings/index.d.ts.map +1 -0
  309. package/dist/tools/learnings/index.js +138 -0
  310. package/dist/tools/learnings/index.js.map +1 -0
  311. package/dist/tools/learnings.d.ts +41 -0
  312. package/dist/tools/learnings.d.ts.map +1 -0
  313. package/dist/tools/learnings.js +149 -0
  314. package/dist/tools/learnings.js.map +1 -0
  315. package/dist/tools/oracle/index.d.ts +6 -0
  316. package/dist/tools/oracle/index.d.ts.map +1 -0
  317. package/dist/tools/oracle/index.js +217 -0
  318. package/dist/tools/oracle/index.js.map +1 -0
  319. package/dist/tools/oracle.d.ts +90 -0
  320. package/dist/tools/oracle.d.ts.map +1 -0
  321. package/dist/tools/oracle.js +529 -0
  322. package/dist/tools/oracle.js.map +1 -0
  323. package/dist/tools/policy.d.ts +119 -0
  324. package/dist/tools/policy.d.ts.map +1 -0
  325. package/dist/tools/policy.js +406 -0
  326. package/dist/tools/policy.js.map +1 -0
  327. package/dist/tools/provenance/index.d.ts +4 -0
  328. package/dist/tools/provenance/index.d.ts.map +1 -0
  329. package/dist/tools/provenance/index.js +63 -0
  330. package/dist/tools/provenance/index.js.map +1 -0
  331. package/dist/tools/provenance.d.ts +75 -0
  332. package/dist/tools/provenance.d.ts.map +1 -0
  333. package/dist/tools/provenance.js +224 -0
  334. package/dist/tools/provenance.js.map +1 -0
  335. package/dist/tools/rateLimiter.d.ts +45 -0
  336. package/dist/tools/rateLimiter.d.ts.map +1 -0
  337. package/dist/tools/rateLimiter.js +91 -0
  338. package/dist/tools/rateLimiter.js.map +1 -0
  339. package/dist/tools/registry/index.d.ts +10 -0
  340. package/dist/tools/registry/index.d.ts.map +1 -0
  341. package/dist/tools/registry/index.js +506 -0
  342. package/dist/tools/registry/index.js.map +1 -0
  343. package/dist/tools/registry.d.ts +3 -0
  344. package/dist/tools/registry.d.ts.map +1 -0
  345. package/dist/tools/registry.js +189 -0
  346. package/dist/tools/registry.js.map +1 -0
  347. package/dist/tools/roadmap/index.d.ts +11 -0
  348. package/dist/tools/roadmap/index.d.ts.map +1 -0
  349. package/dist/tools/roadmap/index.js +364 -0
  350. package/dist/tools/roadmap/index.js.map +1 -0
  351. package/dist/tools/roadmap.d.ts +103 -0
  352. package/dist/tools/roadmap.d.ts.map +1 -0
  353. package/dist/tools/roadmap.js +407 -0
  354. package/dist/tools/roadmap.js.map +1 -0
  355. package/dist/tools/senken.d.ts +11 -0
  356. package/dist/tools/senken.d.ts.map +1 -0
  357. package/dist/tools/senken.js +482 -0
  358. package/dist/tools/senken.js.map +1 -0
  359. package/dist/tools/sentinel/index.d.ts +21 -0
  360. package/dist/tools/sentinel/index.d.ts.map +1 -0
  361. package/dist/tools/sentinel/index.js +1067 -0
  362. package/dist/tools/sentinel/index.js.map +1 -0
  363. package/dist/tools/sentinel-scan-data.d.ts +90 -0
  364. package/dist/tools/sentinel-scan-data.d.ts.map +1 -0
  365. package/dist/tools/sentinel-scan-data.js +122 -0
  366. package/dist/tools/sentinel-scan-data.js.map +1 -0
  367. package/dist/tools/sentinel.d.ts +156 -0
  368. package/dist/tools/sentinel.d.ts.map +1 -0
  369. package/dist/tools/sentinel.js +603 -0
  370. package/dist/tools/sentinel.js.map +1 -0
  371. package/dist/tools/shared/index.d.ts +5 -0
  372. package/dist/tools/shared/index.d.ts.map +1 -0
  373. package/dist/tools/shared/index.js +8 -0
  374. package/dist/tools/shared/index.js.map +1 -0
  375. package/dist/tools/shared/project.d.ts +17 -0
  376. package/dist/tools/shared/project.d.ts.map +1 -0
  377. package/dist/tools/shared/project.js +36 -0
  378. package/dist/tools/shared/project.js.map +1 -0
  379. package/dist/tools/shared/response.d.ts +15 -0
  380. package/dist/tools/shared/response.d.ts.map +1 -0
  381. package/dist/tools/shared/response.js +77 -0
  382. package/dist/tools/shared/response.js.map +1 -0
  383. package/dist/tools/shared/runTracker.d.ts +87 -0
  384. package/dist/tools/shared/runTracker.d.ts.map +1 -0
  385. package/dist/tools/shared/runTracker.js +225 -0
  386. package/dist/tools/shared/runTracker.js.map +1 -0
  387. package/dist/tools/shared/validation.d.ts +10 -0
  388. package/dist/tools/shared/validation.d.ts.map +1 -0
  389. package/dist/tools/shared/validation.js +26 -0
  390. package/dist/tools/shared/validation.js.map +1 -0
  391. package/dist/tools/studio/cloud-spine.d.ts +27 -0
  392. package/dist/tools/studio/cloud-spine.d.ts.map +1 -0
  393. package/dist/tools/studio/cloud-spine.js +845 -0
  394. package/dist/tools/studio/cloud-spine.js.map +1 -0
  395. package/dist/tools/studio/index.d.ts +154 -0
  396. package/dist/tools/studio/index.d.ts.map +1 -0
  397. package/dist/tools/studio/index.js +541 -0
  398. package/dist/tools/studio/index.js.map +1 -0
  399. package/dist/tools/testSpec.d.ts +122 -0
  400. package/dist/tools/testSpec.d.ts.map +1 -0
  401. package/dist/tools/testSpec.js +525 -0
  402. package/dist/tools/testSpec.js.map +1 -0
  403. package/dist/tools/toolsIndex.d.ts +5 -0
  404. package/dist/tools/toolsIndex.d.ts.map +1 -0
  405. package/dist/tools/toolsIndex.js +37 -0
  406. package/dist/tools/toolsIndex.js.map +1 -0
  407. package/dist/tools/types.d.ts +47 -0
  408. package/dist/tools/types.d.ts.map +1 -0
  409. package/dist/tools/types.js +7 -0
  410. package/dist/tools/types.js.map +1 -0
  411. package/dist/tools/vector/index.d.ts +13 -0
  412. package/dist/tools/vector/index.d.ts.map +1 -0
  413. package/dist/tools/vector/index.js +592 -0
  414. package/dist/tools/vector/index.js.map +1 -0
  415. package/dist/tools/vector.d.ts +189 -0
  416. package/dist/tools/vector.d.ts.map +1 -0
  417. package/dist/tools/vector.js +570 -0
  418. package/dist/tools/vector.js.map +1 -0
  419. package/dist/tools/velocity/index.d.ts +9 -0
  420. package/dist/tools/velocity/index.d.ts.map +1 -0
  421. package/dist/tools/velocity/index.js +306 -0
  422. package/dist/tools/velocity/index.js.map +1 -0
  423. package/dist/tools/velocity.d.ts +143 -0
  424. package/dist/tools/velocity.d.ts.map +1 -0
  425. package/dist/tools/velocity.js +628 -0
  426. package/dist/tools/velocity.js.map +1 -0
  427. package/dist/tools/voice/index.d.ts +8 -0
  428. package/dist/tools/voice/index.d.ts.map +1 -0
  429. package/dist/tools/voice/index.js +203 -0
  430. package/dist/tools/voice/index.js.map +1 -0
  431. package/dist/tools/voice.d.ts +291 -0
  432. package/dist/tools/voice.d.ts.map +1 -0
  433. package/dist/tools/voice.js +734 -0
  434. package/dist/tools/voice.js.map +1 -0
  435. package/dist/tools/workflow/index.d.ts +8 -0
  436. package/dist/tools/workflow/index.d.ts.map +1 -0
  437. package/dist/tools/workflow/index.js +199 -0
  438. package/dist/tools/workflow/index.js.map +1 -0
  439. package/dist/tools/workflow.d.ts +123 -0
  440. package/dist/tools/workflow.d.ts.map +1 -0
  441. package/dist/tools/workflow.js +647 -0
  442. package/dist/tools/workflow.js.map +1 -0
  443. package/dist/transports/bridge.d.ts +22 -0
  444. package/dist/transports/bridge.d.ts.map +1 -0
  445. package/dist/transports/bridge.js +177 -0
  446. package/dist/transports/bridge.js.map +1 -0
  447. package/dist/transports/http.d.ts +9 -0
  448. package/dist/transports/http.d.ts.map +1 -0
  449. package/dist/transports/http.js +35 -0
  450. package/dist/transports/http.js.map +1 -0
  451. package/dist/transports/index.d.ts +6 -0
  452. package/dist/transports/index.d.ts.map +1 -0
  453. package/dist/transports/index.js +8 -0
  454. package/dist/transports/index.js.map +1 -0
  455. package/dist/transports/mcp.d.ts +9 -0
  456. package/dist/transports/mcp.d.ts.map +1 -0
  457. package/dist/transports/mcp.js +51 -0
  458. package/dist/transports/mcp.js.map +1 -0
  459. package/dist/transports/stdio.d.ts +9 -0
  460. package/dist/transports/stdio.d.ts.map +1 -0
  461. package/dist/transports/stdio.js +26 -0
  462. package/dist/transports/stdio.js.map +1 -0
  463. package/dist/transports/types.d.ts +27 -0
  464. package/dist/transports/types.d.ts.map +1 -0
  465. package/dist/transports/types.js +8 -0
  466. package/dist/transports/types.js.map +1 -0
  467. package/dist/types/agent-services.d.ts +193 -0
  468. package/dist/types/agent-services.d.ts.map +1 -0
  469. package/dist/types/agent-services.js +8 -0
  470. package/dist/types/agent-services.js.map +1 -0
  471. package/dist/types/index.d.ts +2 -0
  472. package/dist/types/index.d.ts.map +1 -0
  473. package/dist/types/index.js +7 -0
  474. package/dist/types/index.js.map +1 -0
  475. package/package.json +72 -0
  476. package/templates/AGENT.md +87 -0
  477. package/templates/com.decibel.daemon.plist +47 -0
  478. package/templates/sentinel/ISSUE_TEMPLATE.md +20 -0
@@ -0,0 +1,1723 @@
1
+ /**
2
+ * HTTP Server Mode for Decibel MCP
3
+ *
4
+ * Exposes the MCP server over HTTP for remote access (e.g., ChatGPT, external agents).
5
+ *
6
+ * Usage:
7
+ * node dist/server.js --http --port 8787
8
+ * node dist/server.js --http --port 8787 --auth-token YOUR_SECRET
9
+ *
10
+ * Endpoints:
11
+ * GET /health - Health check
12
+ * GET /tools - List available tools
13
+ * POST /call - Execute any tool: { tool: string, arguments: object }
14
+ * POST /dojo/wish - Shorthand for dojo_add_wish
15
+ * POST /dojo/propose - Shorthand for dojo_create_proposal
16
+ * POST /dojo/scaffold - Shorthand for dojo_scaffold_experiment
17
+ * POST /dojo/run - Shorthand for dojo_run_experiment
18
+ * POST /dojo/results - Shorthand for dojo_get_results
19
+ * POST /dojo/artifact - Shorthand for dojo_read_artifact
20
+ * GET /dojo/list - Shorthand for dojo_list
21
+ * POST /mcp - Full MCP protocol endpoint
22
+ *
23
+ * All responses use status envelope:
24
+ * { "status": "executed", ...data }
25
+ * { "status": "error", "error": "...", "code": "..." }
26
+ */
27
+ import { createServer } from 'http';
28
+ import { timingSafeEqual } from 'crypto';
29
+ import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js';
30
+ import { readFileSync } from 'fs';
31
+ import { fileURLToPath } from 'url';
32
+ import { dirname, join } from 'path';
33
+ import { log } from './config.js';
34
+ import { isSupabaseConfigured } from './lib/supabase.js';
35
+ import { getLicenseValidator } from './license.js';
36
+ import { listProjects } from './projectRegistry.js';
37
+ import { listEpics, listRepoIssues, isProjectResolutionError, } from './tools/sentinel.js';
38
+ import { voiceInboxAdd, } from './tools/voice.js';
39
+ import { generateImage, getImageStatus, meshyGenerate, getMeshyStatus, meshyDownload, tripoGenerate, getTripoStatus, tripoDownload, klingGenerateVideo, klingGenerateTextVideo, klingGenerateAvatar, getKlingStatus, listTasks, } from './tools/studio/index.js';
40
+ // Module-level kernel reference — set by startHttpServer()
41
+ let kernel;
42
+ let landingPageHtml = '';
43
+ let startedAt = 0;
44
+ let sseConnectionCount = 0;
45
+ // ============================================================================
46
+ // Security: Body Size Limit
47
+ // ============================================================================
48
+ const MAX_BODY_BYTES = 1_048_576; // 1MB
49
+ // ============================================================================
50
+ // Security: Rate Limiter
51
+ // ============================================================================
52
+ class RateLimiter {
53
+ windows = new Map();
54
+ maxRpm;
55
+ constructor(maxRpm) {
56
+ this.maxRpm = maxRpm;
57
+ }
58
+ /** Returns true if the request should be allowed */
59
+ check(ip) {
60
+ const now = Date.now();
61
+ const entry = this.windows.get(ip);
62
+ if (!entry || now - entry.start > 60_000) {
63
+ this.windows.set(ip, { count: 1, start: now });
64
+ return true;
65
+ }
66
+ entry.count++;
67
+ return entry.count <= this.maxRpm;
68
+ }
69
+ /** Update the max RPM (e.g. on config reload) */
70
+ setMaxRpm(rpm) {
71
+ this.maxRpm = rpm;
72
+ }
73
+ /** Periodic cleanup of expired windows */
74
+ cleanup() {
75
+ const now = Date.now();
76
+ for (const [ip, entry] of this.windows) {
77
+ if (now - entry.start > 60_000) {
78
+ this.windows.delete(ip);
79
+ }
80
+ }
81
+ }
82
+ }
83
+ // ============================================================================
84
+ // Security: Timing-Safe Token Comparison
85
+ // ============================================================================
86
+ function timingSafeTokenCompare(provided, expected) {
87
+ const a = Buffer.from(provided, 'utf-8');
88
+ const b = Buffer.from(expected, 'utf-8');
89
+ if (a.length !== b.length) {
90
+ // Still do a comparison to keep timing constant
91
+ timingSafeEqual(a, a);
92
+ return false;
93
+ }
94
+ return timingSafeEqual(a, b);
95
+ }
96
+ // ============================================================================
97
+ // Version Info
98
+ // ============================================================================
99
+ const __filename = fileURLToPath(import.meta.url);
100
+ const __dirname = dirname(__filename);
101
+ function getVersion() {
102
+ try {
103
+ // Try to read from package.json (works in both dev and prod)
104
+ const pkgPath = join(__dirname, '..', 'package.json');
105
+ const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'));
106
+ return { version: pkg.version || '0.0.0', name: pkg.name || '@decibel/tools' };
107
+ }
108
+ catch {
109
+ return { version: '2.0.0', name: '@decibel/tools' };
110
+ }
111
+ }
112
+ const PKG = getVersion();
113
+ // ============================================================================
114
+ // Landing Page HTML
115
+ // ============================================================================
116
+ /**
117
+ * Generate landing page HTML from facade definitions.
118
+ */
119
+ function buildLandingPageHtml(facades) {
120
+ const escHtml = (s) => s.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;');
121
+ const totalActions = facades.reduce((sum, f) => sum + f.actions.length, 0);
122
+ const toolSections = facades
123
+ .sort((a, b) => a.name.localeCompare(b.name))
124
+ .map(f => {
125
+ const rows = f.actions
126
+ .sort()
127
+ .map(action => {
128
+ return ` <tr><td class="tool-name">${escHtml(f.name)}(${escHtml(action)})</td><td class="tool-desc"></td></tr>`;
129
+ })
130
+ .join('\n');
131
+ return ` <div class="module-section">
132
+ <h3>${escHtml(f.name)} <span class="tool-count">${f.actions.length} actions</span></h3>
133
+ <p style="color:#888;font-size:0.85rem;margin-bottom:0.75rem">${escHtml(f.description.split('.')[0])}</p>
134
+ <table>${rows}</table>
135
+ </div>`;
136
+ }).join('\n');
137
+ return `<!DOCTYPE html>
138
+ <html lang="en">
139
+ <head>
140
+ <meta charset="UTF-8">
141
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
142
+ <title>Decibel Tools - ${facades.length} Facades</title>
143
+ <style>
144
+ * { box-sizing: border-box; margin: 0; padding: 0; }
145
+ body {
146
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, sans-serif;
147
+ background: #0a0a0a;
148
+ color: #e5e5e5;
149
+ min-height: 100vh;
150
+ display: flex;
151
+ flex-direction: column;
152
+ align-items: center;
153
+ padding: 2rem;
154
+ }
155
+ .container { max-width: 900px; width: 100%; }
156
+ h1 {
157
+ font-size: 2.5rem;
158
+ font-weight: 700;
159
+ margin-bottom: 0.5rem;
160
+ background: linear-gradient(135deg, #fff 0%, #888 100%);
161
+ -webkit-background-clip: text;
162
+ -webkit-text-fill-color: transparent;
163
+ }
164
+ .tagline { font-size: 1.25rem; color: #888; margin-bottom: 0.5rem; }
165
+ .tool-total { font-size: 0.9rem; color: #555; margin-bottom: 2rem; }
166
+ .features { display: grid; grid-template-columns: repeat(2, 1fr); gap: 1rem; margin-bottom: 2rem; }
167
+ .feature { background: #111; border: 1px solid #222; border-radius: 8px; padding: 1.25rem; }
168
+ .feature h3 { font-size: 0.9rem; font-weight: 600; margin-bottom: 0.5rem; color: #fff; }
169
+ .feature p { font-size: 0.85rem; color: #888; line-height: 1.5; }
170
+ .module-section { background: #111; border: 1px solid #222; border-radius: 8px; padding: 1.25rem; margin-bottom: 1rem; }
171
+ .module-section h3 { font-size: 0.95rem; font-weight: 600; margin-bottom: 0.75rem; color: #fff; text-transform: capitalize; }
172
+ .tool-count { font-size: 0.75rem; color: #555; font-weight: 400; margin-left: 0.5rem; }
173
+ table { width: 100%; border-collapse: collapse; }
174
+ tr { border-bottom: 1px solid #1a1a1a; }
175
+ tr:last-child { border-bottom: none; }
176
+ td { padding: 0.4rem 0; vertical-align: top; font-size: 0.8rem; }
177
+ .tool-name { color: #ccc; font-family: 'SF Mono', Monaco, 'Cascadia Code', monospace; white-space: nowrap; padding-right: 1rem; width: 1%; }
178
+ .tool-desc { color: #666; }
179
+ footer { margin-top: 2rem; padding-top: 2rem; font-size: 0.8rem; color: #444; }
180
+ footer a { color: #666; text-decoration: none; }
181
+ footer a:hover { color: #888; }
182
+ @media (max-width: 600px) { .features { grid-template-columns: 1fr; } }
183
+ </style>
184
+ </head>
185
+ <body>
186
+ <div class="container">
187
+ <h1>Decibel Tools</h1>
188
+ <p class="tagline">Project intelligence for AI coding agents</p>
189
+ <p class="tool-total">${facades.length} facades, ${totalActions} actions &middot; v${PKG.version}</p>
190
+ <div class="features">
191
+ <div class="feature"><h3>Sentinel</h3><p>Track epics, issues, and incidents. Your agent knows what's in flight.</p></div>
192
+ <div class="feature"><h3>Architect</h3><p>Record ADRs and decisions. Context persists across sessions.</p></div>
193
+ <div class="feature"><h3>Dojo</h3><p>Incubate ideas with wishes, proposals, and experiments.</p></div>
194
+ <div class="feature"><h3>Oracle</h3><p>Get AI-powered recommendations on what to work on next.</p></div>
195
+ </div>
196
+ ${toolSections}
197
+ <footer>
198
+ <a href="https://github.com/decibelsystems/decibel-tools-beta">GitHub</a> &middot;
199
+ <a href="https://modelcontextprotocol.io">MCP Protocol</a>
200
+ </footer>
201
+ </div>
202
+ </body>
203
+ </html>`;
204
+ }
205
+ // ============================================================================
206
+ // Response Helpers
207
+ // ============================================================================
208
+ /**
209
+ * Format milliseconds into human-readable uptime string.
210
+ */
211
+ function formatUptime(ms) {
212
+ const seconds = Math.floor(ms / 1000);
213
+ const minutes = Math.floor(seconds / 60);
214
+ const hours = Math.floor(minutes / 60);
215
+ const days = Math.floor(hours / 24);
216
+ if (days > 0)
217
+ return `${days}d ${hours % 24}h ${minutes % 60}m`;
218
+ if (hours > 0)
219
+ return `${hours}h ${minutes % 60}m`;
220
+ if (minutes > 0)
221
+ return `${minutes}m ${seconds % 60}s`;
222
+ return `${seconds}s`;
223
+ }
224
+ /**
225
+ * Wrap a successful result in status envelope
226
+ */
227
+ function wrapSuccess(data) {
228
+ return { status: 'executed', ...data };
229
+ }
230
+ /**
231
+ * Wrap an error in status envelope
232
+ */
233
+ function wrapError(error, code) {
234
+ return { status: 'error', error, ...(code && { code }) };
235
+ }
236
+ /**
237
+ * Send JSON response with status envelope
238
+ */
239
+ function sendJson(res, statusCode, data) {
240
+ res.writeHead(statusCode, { 'Content-Type': 'application/json' });
241
+ res.end(JSON.stringify(data));
242
+ }
243
+ /**
244
+ * Parse JSON body from request (with 1MB size limit)
245
+ */
246
+ async function parseBody(req) {
247
+ return new Promise((resolve, reject) => {
248
+ let body = '';
249
+ let bytes = 0;
250
+ req.on('data', (chunk) => {
251
+ bytes += typeof chunk === 'string' ? Buffer.byteLength(chunk) : chunk.length;
252
+ if (bytes > MAX_BODY_BYTES) {
253
+ req.destroy();
254
+ reject(new Error('Request body too large (max 1MB)'));
255
+ return;
256
+ }
257
+ body += chunk;
258
+ });
259
+ req.on('end', () => {
260
+ try {
261
+ resolve(body ? JSON.parse(body) : {});
262
+ }
263
+ catch {
264
+ reject(new Error('Invalid JSON'));
265
+ }
266
+ });
267
+ req.on('error', reject);
268
+ });
269
+ }
270
+ // ============================================================================
271
+ // Tool Executor — unified dispatch through modular tool registry
272
+ // ============================================================================
273
+ /**
274
+ * Execute any tool via the kernel's dispatch.
275
+ * Extracts agent context from HTTP headers when present.
276
+ */
277
+ async function executeTool(tool, args, req, tierOverride) {
278
+ try {
279
+ // Extract agent context from HTTP headers
280
+ const context = req ? {
281
+ agentId: req.headers['x-agent-id'],
282
+ runId: req.headers['x-run-id'],
283
+ parentCallId: req.headers['x-parent-call-id'],
284
+ scope: req.headers['x-scope'],
285
+ tier: tierOverride,
286
+ } : tierOverride ? { tier: tierOverride } : undefined;
287
+ const toolResult = await kernel.dispatch(tool, args, context);
288
+ const text = toolResult.content[0]?.text;
289
+ if (toolResult.isError) {
290
+ return wrapError(text || 'Tool execution failed', 'TOOL_ERROR');
291
+ }
292
+ // Parse JSON result or wrap as message
293
+ let result;
294
+ if (text) {
295
+ try {
296
+ result = JSON.parse(text);
297
+ }
298
+ catch {
299
+ result = { message: text };
300
+ }
301
+ }
302
+ else {
303
+ result = { success: true };
304
+ }
305
+ return wrapSuccess(result);
306
+ }
307
+ catch (error) {
308
+ const message = error instanceof Error ? error.message : String(error);
309
+ if (message.includes('Rate limit')) {
310
+ return wrapError(message, 'RATE_LIMITED');
311
+ }
312
+ if (message.includes('Access denied')) {
313
+ return wrapError(message, 'ACCESS_DENIED');
314
+ }
315
+ if (message.includes('not found')) {
316
+ return wrapError(message, 'NOT_FOUND');
317
+ }
318
+ return wrapError(message, 'EXECUTION_ERROR');
319
+ }
320
+ }
321
+ /**
322
+ * Get list of available facades — public API for tool discovery
323
+ */
324
+ function getAvailableTools() {
325
+ return kernel.facades.map(f => ({
326
+ name: f.name,
327
+ description: f.description,
328
+ actions: Object.keys(f.actions),
329
+ }));
330
+ }
331
+ /**
332
+ * Get tools in OpenAI function calling format (facade-based)
333
+ */
334
+ function getOpenAITools() {
335
+ return kernel.getMcpToolDefinitions('full').map(def => ({
336
+ type: 'function',
337
+ function: {
338
+ name: def.name,
339
+ description: def.description,
340
+ parameters: {
341
+ type: 'object',
342
+ properties: def.inputSchema.properties,
343
+ required: def.inputSchema.required,
344
+ },
345
+ },
346
+ }));
347
+ }
348
+ // ============================================================================
349
+ // License Tier Resolution
350
+ // ============================================================================
351
+ /**
352
+ * Resolve the caller's license tier from the request.
353
+ * - DECIBEL_PRO=1 env var → skip validation (dev mode)
354
+ * - Authorization header with DCBL-XXXX key → validate via LicenseValidator
355
+ * - No key → 'core' tier (only core facades)
356
+ * - Config-level key → use that as default
357
+ */
358
+ async function resolveTier(req, configLicenseKey) {
359
+ // Dev mode bypass
360
+ if (process.env.DECIBEL_PRO === '1' || process.env.NODE_ENV !== 'production') {
361
+ return 'pro';
362
+ }
363
+ // Extract license key from Authorization header (separate from auth token)
364
+ // Format: X-License-Key: DCBL-XXXX-XXXX-XXXX
365
+ const licenseHeader = req.headers['x-license-key'];
366
+ const key = licenseHeader || configLicenseKey;
367
+ if (!key)
368
+ return 'core';
369
+ const validator = getLicenseValidator();
370
+ const result = await validator.validate(key);
371
+ return result.valid ? result.tier : 'core';
372
+ }
373
+ /**
374
+ * Start an HTTP server that handles MCP requests
375
+ *
376
+ * Note: This creates a single stateless transport. Each request is handled
377
+ * independently. For full session support, this would need to be expanded.
378
+ */
379
+ export async function startHttpServer(server, kernelInstance, options) {
380
+ const { port, authToken, host = '0.0.0.0', sseKeepaliveMs = 30000, // Send keepalive every 30s
381
+ timeoutMs = 120000, // 2 minute default timeout
382
+ retryIntervalMs = 3000, // 3s retry for SSE clients
383
+ rateLimitRpm = 100, // 100 req/min per IP default
384
+ isDaemon = false, configLicenseKey, } = options;
385
+ // Set module-level references
386
+ kernel = kernelInstance;
387
+ startedAt = Date.now();
388
+ log(`HTTP: Using kernel with ${kernel.toolCount} tools`);
389
+ // Rate limiter (clean up stale entries every 60s)
390
+ const rateLimiter = new RateLimiter(rateLimitRpm);
391
+ const rateLimiterCleanup = setInterval(() => rateLimiter.cleanup(), 60_000);
392
+ // Build landing page from actual tool list
393
+ landingPageHtml = buildLandingPageHtml(getAvailableTools());
394
+ // Create transport in STATELESS mode (better for ChatGPT compatibility)
395
+ // Setting sessionIdGenerator to undefined disables session tracking
396
+ const transport = new StreamableHTTPServerTransport({
397
+ sessionIdGenerator: undefined, // Stateless mode
398
+ enableJsonResponse: true, // Enable JSON fallback for non-streaming clients
399
+ retryInterval: retryIntervalMs, // Tell clients how long to wait before retry
400
+ });
401
+ // Connect the MCP server to the transport
402
+ await server.connect(transport);
403
+ // Track active SSE connections for keepalive
404
+ const activeSseConnections = new Set();
405
+ // Track active in-flight requests for graceful shutdown
406
+ const activeRequests = new Set();
407
+ // Start SSE keepalive heartbeat
408
+ const keepaliveInterval = setInterval(() => {
409
+ if (activeSseConnections.size > 0) {
410
+ log(`SSE keepalive: pinging ${activeSseConnections.size} connection(s)`);
411
+ }
412
+ for (const res of activeSseConnections) {
413
+ try {
414
+ if (!res.writableEnded) {
415
+ // Send SSE comment as keepalive (standard pattern)
416
+ res.write(': keepalive\n\n');
417
+ }
418
+ else {
419
+ activeSseConnections.delete(res);
420
+ }
421
+ }
422
+ catch (e) {
423
+ // Connection likely closed
424
+ activeSseConnections.delete(res);
425
+ }
426
+ }
427
+ }, sseKeepaliveMs);
428
+ // Clean up on process exit
429
+ process.on('SIGTERM', () => {
430
+ clearInterval(keepaliveInterval);
431
+ });
432
+ const httpServer = createServer(async (req, res) => {
433
+ const url = new URL(req.url || '/', `http://${req.headers.host}`);
434
+ const path = url.pathname;
435
+ log(`HTTP: ${req.method} ${path}`);
436
+ // CORS headers — /mcp needs '*' for ChatGPT; REST endpoints restrict to localhost in daemon mode
437
+ const isMcpRoute = path === '/mcp' || path === '/sse' || path === '/sse/';
438
+ if (isMcpRoute || !isDaemon) {
439
+ res.setHeader('Access-Control-Allow-Origin', '*');
440
+ }
441
+ else {
442
+ // Daemon mode: restrict REST endpoints to localhost origins
443
+ const origin = req.headers.origin || '';
444
+ const localhostOrigins = ['http://localhost', 'http://127.0.0.1', 'https://localhost', 'https://127.0.0.1'];
445
+ if (localhostOrigins.some(lo => origin.startsWith(lo)) || !origin) {
446
+ res.setHeader('Access-Control-Allow-Origin', origin || 'http://localhost');
447
+ }
448
+ }
449
+ res.setHeader('Access-Control-Allow-Methods', 'GET, POST, DELETE, OPTIONS');
450
+ res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization, Mcp-Session-Id, Accept');
451
+ res.setHeader('Access-Control-Expose-Headers', 'Mcp-Session-Id');
452
+ // (a) Handle preflight OPTIONS requests
453
+ if (req.method === 'OPTIONS') {
454
+ res.writeHead(204);
455
+ res.end();
456
+ return;
457
+ }
458
+ // Rate limiting (check before auth to prevent brute force)
459
+ const clientIp = (req.socket.remoteAddress || '127.0.0.1').replace('::ffff:', '');
460
+ if (!rateLimiter.check(clientIp)) {
461
+ log(`HTTP: Rate limited ${clientIp}`);
462
+ sendJson(res, 429, wrapError('Too many requests (rate limit exceeded)', 'RATE_LIMITED'));
463
+ return;
464
+ }
465
+ // Track in-flight request
466
+ activeRequests.add(req);
467
+ res.on('finish', () => activeRequests.delete(req));
468
+ res.on('close', () => activeRequests.delete(req));
469
+ // (c) Root health check - GET / returns 200
470
+ if (path === '/' && req.method === 'GET') {
471
+ res.writeHead(200, { 'Content-Type': 'application/json' });
472
+ res.end(JSON.stringify({
473
+ status: 'ok',
474
+ name: PKG.name,
475
+ version: PKG.version,
476
+ api_version: 'v1',
477
+ }));
478
+ return;
479
+ }
480
+ // Health check at /health too
481
+ if (path === '/health') {
482
+ const uptimeMs = Date.now() - startedAt;
483
+ // Determine pro status from config license key
484
+ const proEnabled = process.env.DECIBEL_PRO === '1' || process.env.NODE_ENV !== 'production';
485
+ let licenseTier = proEnabled ? 'pro' : 'core';
486
+ if (configLicenseKey && !proEnabled) {
487
+ const cached = getLicenseValidator().getCachedResult(configLicenseKey);
488
+ if (cached)
489
+ licenseTier = cached.tier;
490
+ }
491
+ res.writeHead(200, { 'Content-Type': 'application/json' });
492
+ res.end(JSON.stringify({
493
+ status: 'ok',
494
+ version: PKG.version,
495
+ api_version: 'v1',
496
+ uptime_ms: uptimeMs,
497
+ uptime_human: formatUptime(uptimeMs),
498
+ pid: process.pid,
499
+ facade_count: kernel.facadeCount,
500
+ internal_tool_count: kernel.toolCount,
501
+ connected_clients: activeSseConnections.size,
502
+ active_requests: activeRequests.size,
503
+ pro: licenseTier !== 'core',
504
+ license_tier: licenseTier,
505
+ supabase_configured: isSupabaseConfigured(),
506
+ }));
507
+ return;
508
+ }
509
+ // Readiness probe at /ready
510
+ if (path === '/ready') {
511
+ // Ready if kernel loaded and at least one facade is available
512
+ const ready = kernel && kernel.facadeCount > 0;
513
+ res.writeHead(ready ? 200 : 503, { 'Content-Type': 'application/json' });
514
+ res.end(JSON.stringify({
515
+ ready,
516
+ facade_count: kernel?.facadeCount || 0,
517
+ }));
518
+ return;
519
+ }
520
+ // GET /events — query dispatch event log (dispatch.jsonl)
521
+ if (path === '/events' && req.method === 'GET') {
522
+ const dispatchLogPath = join(process.env.HOME || '~', '.decibel', 'logs', 'dispatch.jsonl');
523
+ try {
524
+ const content = readFileSync(dispatchLogPath, 'utf-8');
525
+ const lines = content.trim().split('\n').filter(Boolean);
526
+ // Parse query params from URL
527
+ const since = url.searchParams.get('since');
528
+ const agentFilter = url.searchParams.get('agent_id');
529
+ const limitParam = url.searchParams.get('limit');
530
+ const limit = limitParam ? parseInt(limitParam, 10) : 100;
531
+ let events = lines.map(line => {
532
+ try {
533
+ return JSON.parse(line);
534
+ }
535
+ catch {
536
+ return null;
537
+ }
538
+ }).filter(Boolean);
539
+ // Filter by timestamp
540
+ if (since) {
541
+ events = events.filter((e) => e.timestamp >= since);
542
+ }
543
+ // Filter by agent
544
+ if (agentFilter) {
545
+ events = events.filter((e) => e.agentId === agentFilter);
546
+ }
547
+ // Limit + return most recent
548
+ const recent = events.slice(-limit);
549
+ res.writeHead(200, { 'Content-Type': 'application/json' });
550
+ res.end(JSON.stringify({ events: recent, total: events.length }));
551
+ }
552
+ catch {
553
+ // No dispatch log yet — empty response
554
+ res.writeHead(200, { 'Content-Type': 'application/json' });
555
+ res.end(JSON.stringify({ events: [], total: 0 }));
556
+ }
557
+ return;
558
+ }
559
+ // Landing page at /docs (always HTML)
560
+ if (path === '/docs' && req.method === 'GET') {
561
+ res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
562
+ res.end(landingPageHtml);
563
+ return;
564
+ }
565
+ // GET /tools — HTML for browsers, JSON for API clients
566
+ if (path === '/tools' && req.method === 'GET') {
567
+ const accept = req.headers.accept || '';
568
+ if (accept.includes('text/html')) {
569
+ res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
570
+ res.end(landingPageHtml);
571
+ return;
572
+ }
573
+ // JSON response (for curl, agents, etc.) — falls through to auth + tool list below
574
+ }
575
+ // Serve OpenAPI spec for ChatGPT Actions (handle GET and POST)
576
+ if ((path === '/openapi.yaml' || path === '/openapi.json') && (req.method === 'GET' || req.method === 'POST')) {
577
+ try {
578
+ const specPath = join(__dirname, '..', 'openapi.yaml');
579
+ const spec = readFileSync(specPath, 'utf-8');
580
+ if (path === '/openapi.json') {
581
+ // Convert YAML to JSON if requested
582
+ const yaml = await import('yaml');
583
+ const parsed = yaml.parse(spec);
584
+ res.writeHead(200, { 'Content-Type': 'application/json' });
585
+ res.end(JSON.stringify(parsed, null, 2));
586
+ }
587
+ else {
588
+ res.writeHead(200, { 'Content-Type': 'text/yaml' });
589
+ res.end(spec);
590
+ }
591
+ }
592
+ catch (error) {
593
+ res.writeHead(404, { 'Content-Type': 'application/json' });
594
+ res.end(JSON.stringify({ error: 'OpenAPI spec not found' }));
595
+ }
596
+ return;
597
+ }
598
+ // (d) OAuth discovery routes - return 404 (not 400) to keep connector wizard happy
599
+ if (path === '/.well-known/oauth-authorization-server' ||
600
+ path === '/.well-known/openid-configuration' ||
601
+ path === '/oauth/authorize' ||
602
+ path === '/oauth/token' ||
603
+ path === '/oauth/register') {
604
+ res.writeHead(404, { 'Content-Type': 'application/json' });
605
+ res.end(JSON.stringify({ error: 'Not found' }));
606
+ return;
607
+ }
608
+ // Auth check (timing-safe comparison to prevent timing attacks)
609
+ if (authToken) {
610
+ const authHeader = req.headers.authorization;
611
+ if (!authHeader || !timingSafeTokenCompare(authHeader, `Bearer ${authToken}`)) {
612
+ log('HTTP: Unauthorized request');
613
+ sendJson(res, 401, wrapError('Unauthorized', 'UNAUTHORIZED'));
614
+ return;
615
+ }
616
+ }
617
+ // ========================================================================
618
+ // Simple REST Endpoints (for external AI agents)
619
+ // ========================================================================
620
+ // GET /tools - List available tools
621
+ if (path === '/tools' && req.method === 'GET') {
622
+ sendJson(res, 200, wrapSuccess({
623
+ version: PKG.version,
624
+ api_version: 'v1',
625
+ tools: getAvailableTools(),
626
+ }));
627
+ return;
628
+ }
629
+ // GET /facades - Facade registry for agent bootstrap
630
+ if (path === '/facades' && req.method === 'GET') {
631
+ const tier = (url.searchParams.get('tier') || 'full');
632
+ sendJson(res, 200, wrapSuccess({
633
+ facades: kernel.facades
634
+ .filter(f => tier !== 'micro' || f.microEligible)
635
+ .map(f => ({
636
+ name: f.name,
637
+ description: tier === 'compact' ? f.compactDescription : f.description,
638
+ actions: Object.keys(f.actions),
639
+ tier: f.tier,
640
+ })),
641
+ tier,
642
+ facade_count: kernel.facadeCount,
643
+ internal_tool_count: kernel.toolCount,
644
+ }));
645
+ return;
646
+ }
647
+ // POST /call - Execute any tool
648
+ if (path === '/call' && req.method === 'POST') {
649
+ try {
650
+ const body = await parseBody(req);
651
+ const tool = body.tool;
652
+ const args = (body.arguments || {});
653
+ if (!tool) {
654
+ sendJson(res, 400, wrapError('Missing "tool" field', 'MISSING_TOOL'));
655
+ return;
656
+ }
657
+ const tier = await resolveTier(req, configLicenseKey);
658
+ log(`HTTP: /call tool=${tool} tier=${tier}`);
659
+ const result = await executeTool(tool, args, req, tier);
660
+ sendJson(res, result.status === 'error' ? 400 : 200, result);
661
+ }
662
+ catch (error) {
663
+ const message = error instanceof Error ? error.message : String(error);
664
+ if (message.includes('too large')) {
665
+ sendJson(res, 413, wrapError(message, 'BODY_TOO_LARGE'));
666
+ }
667
+ else {
668
+ sendJson(res, 400, wrapError(message, 'PARSE_ERROR'));
669
+ }
670
+ }
671
+ return;
672
+ }
673
+ // POST /batch - Dispatch multiple independent calls in parallel
674
+ if (path === '/batch' && req.method === 'POST') {
675
+ try {
676
+ const body = await parseBody(req);
677
+ const calls = body.calls;
678
+ if (!Array.isArray(calls) || calls.length === 0) {
679
+ sendJson(res, 400, wrapError('Missing or empty "calls" array', 'INVALID_BATCH'));
680
+ return;
681
+ }
682
+ if (calls.length > 20) {
683
+ sendJson(res, 400, wrapError('Batch limited to 20 calls', 'BATCH_TOO_LARGE'));
684
+ return;
685
+ }
686
+ const tier = await resolveTier(req, configLicenseKey);
687
+ // Build context from headers + optional body context
688
+ const bodyContext = (body.context || {});
689
+ const context = {
690
+ agentId: req.headers['x-agent-id'] || bodyContext.agentId,
691
+ runId: req.headers['x-run-id'] || bodyContext.runId,
692
+ parentCallId: req.headers['x-parent-call-id'] || bodyContext.parentCallId,
693
+ scope: req.headers['x-scope'] || bodyContext.scope,
694
+ allowedFacades: bodyContext.allowedFacades,
695
+ tier,
696
+ };
697
+ log(`HTTP: /batch — ${calls.length} calls (agent=${context.agentId || 'anonymous'}, tier=${tier})`);
698
+ const results = await kernel.batch(calls, context);
699
+ sendJson(res, 200, { status: 'executed', results });
700
+ }
701
+ catch (error) {
702
+ const message = error instanceof Error ? error.message : String(error);
703
+ sendJson(res, 400, wrapError(message, 'BATCH_ERROR'));
704
+ }
705
+ return;
706
+ }
707
+ // ========================================================================
708
+ // OpenAI-Compatible REST API (for SDK function calling)
709
+ // ========================================================================
710
+ // GET /api/tools - List tools in OpenAI function calling format
711
+ if (path === '/api/tools' && req.method === 'GET') {
712
+ const tools = getOpenAITools();
713
+ res.writeHead(200, { 'Content-Type': 'application/json' });
714
+ res.end(JSON.stringify(tools));
715
+ return;
716
+ }
717
+ // POST /api/tools/{name} - Execute a tool by name
718
+ if (path.startsWith('/api/tools/') && req.method === 'POST') {
719
+ try {
720
+ const toolName = path.replace('/api/tools/', '');
721
+ if (!toolName) {
722
+ sendJson(res, 400, wrapError('Missing tool name in path', 'MISSING_TOOL_NAME'));
723
+ return;
724
+ }
725
+ const body = await parseBody(req);
726
+ log(`HTTP: /api/tools/${toolName}`);
727
+ const result = await executeTool(toolName, body);
728
+ sendJson(res, result.status === 'error' ? 400 : 200, result);
729
+ }
730
+ catch (error) {
731
+ const message = error instanceof Error ? error.message : String(error);
732
+ sendJson(res, 400, wrapError(message, 'EXECUTION_ERROR'));
733
+ }
734
+ return;
735
+ }
736
+ // ========================================================================
737
+ // Dojo Convenience Endpoints
738
+ // ========================================================================
739
+ // POST /dojo/wish - Add a wish
740
+ if (path === '/dojo/wish' && req.method === 'POST') {
741
+ try {
742
+ const body = await parseBody(req);
743
+ log('HTTP: /dojo/wish');
744
+ const result = await executeTool('dojo_add_wish', body);
745
+ sendJson(res, result.status === 'error' ? 400 : 200, result);
746
+ }
747
+ catch (error) {
748
+ const message = error instanceof Error ? error.message : String(error);
749
+ sendJson(res, 400, wrapError(message, 'PARSE_ERROR'));
750
+ }
751
+ return;
752
+ }
753
+ // POST /dojo/propose - Create a proposal
754
+ if (path === '/dojo/propose' && req.method === 'POST') {
755
+ try {
756
+ const body = await parseBody(req);
757
+ log('HTTP: /dojo/propose');
758
+ const result = await executeTool('dojo_create_proposal', body);
759
+ sendJson(res, result.status === 'error' ? 400 : 200, result);
760
+ }
761
+ catch (error) {
762
+ const message = error instanceof Error ? error.message : String(error);
763
+ sendJson(res, 400, wrapError(message, 'PARSE_ERROR'));
764
+ }
765
+ return;
766
+ }
767
+ // POST /dojo/scaffold - Scaffold experiment
768
+ if (path === '/dojo/scaffold' && req.method === 'POST') {
769
+ try {
770
+ const body = await parseBody(req);
771
+ log('HTTP: /dojo/scaffold');
772
+ const result = await executeTool('dojo_scaffold_experiment', body);
773
+ sendJson(res, result.status === 'error' ? 400 : 200, result);
774
+ }
775
+ catch (error) {
776
+ const message = error instanceof Error ? error.message : String(error);
777
+ sendJson(res, 400, wrapError(message, 'PARSE_ERROR'));
778
+ }
779
+ return;
780
+ }
781
+ // POST /dojo/run - Run experiment
782
+ if (path === '/dojo/run' && req.method === 'POST') {
783
+ try {
784
+ const body = await parseBody(req);
785
+ log('HTTP: /dojo/run');
786
+ const result = await executeTool('dojo_run_experiment', body);
787
+ sendJson(res, result.status === 'error' ? 400 : 200, result);
788
+ }
789
+ catch (error) {
790
+ const message = error instanceof Error ? error.message : String(error);
791
+ sendJson(res, 400, wrapError(message, 'PARSE_ERROR'));
792
+ }
793
+ return;
794
+ }
795
+ // POST /dojo/results - Get experiment results
796
+ if (path === '/dojo/results' && req.method === 'POST') {
797
+ try {
798
+ const body = await parseBody(req);
799
+ log('HTTP: /dojo/results');
800
+ const result = await executeTool('dojo_read_results', body);
801
+ sendJson(res, result.status === 'error' ? 400 : 200, result);
802
+ }
803
+ catch (error) {
804
+ const message = error instanceof Error ? error.message : String(error);
805
+ sendJson(res, 400, wrapError(message, 'PARSE_ERROR'));
806
+ }
807
+ return;
808
+ }
809
+ // GET /dojo/list - List all (or POST with filter)
810
+ if (path === '/dojo/list') {
811
+ try {
812
+ const body = req.method === 'POST' ? await parseBody(req) : {};
813
+ // For GET, try to get project_id from query params
814
+ if (req.method === 'GET') {
815
+ const projectId = url.searchParams.get('project_id');
816
+ if (projectId) {
817
+ body.project_id = projectId;
818
+ }
819
+ }
820
+ log('HTTP: /dojo/list');
821
+ const result = await executeTool('dojo_list', body);
822
+ sendJson(res, result.status === 'error' ? 400 : 200, result);
823
+ }
824
+ catch (error) {
825
+ const message = error instanceof Error ? error.message : String(error);
826
+ sendJson(res, 400, wrapError(message, 'PARSE_ERROR'));
827
+ }
828
+ return;
829
+ }
830
+ // GET /dojo/wishes - List wishes
831
+ if (path === '/dojo/wishes') {
832
+ try {
833
+ const body = req.method === 'POST' ? await parseBody(req) : {};
834
+ if (req.method === 'GET') {
835
+ const projectId = url.searchParams.get('project_id');
836
+ if (projectId) {
837
+ body.project_id = projectId;
838
+ }
839
+ }
840
+ log('HTTP: /dojo/wishes');
841
+ const result = await executeTool('dojo_list_wishes', body);
842
+ sendJson(res, result.status === 'error' ? 400 : 200, result);
843
+ }
844
+ catch (error) {
845
+ const message = error instanceof Error ? error.message : String(error);
846
+ sendJson(res, 400, wrapError(message, 'PARSE_ERROR'));
847
+ }
848
+ return;
849
+ }
850
+ // POST /dojo/can-graduate - Check graduation eligibility
851
+ if (path === '/dojo/can-graduate' && req.method === 'POST') {
852
+ try {
853
+ const body = await parseBody(req);
854
+ log('HTTP: /dojo/can-graduate');
855
+ const result = await executeTool('dojo_can_graduate', body);
856
+ sendJson(res, result.status === 'error' ? 400 : 200, result);
857
+ }
858
+ catch (error) {
859
+ const message = error instanceof Error ? error.message : String(error);
860
+ sendJson(res, 400, wrapError(message, 'PARSE_ERROR'));
861
+ }
862
+ return;
863
+ }
864
+ // POST /dojo/artifact - Read artifact from experiment results
865
+ if (path === '/dojo/artifact' && req.method === 'POST') {
866
+ try {
867
+ const body = await parseBody(req);
868
+ log('HTTP: /dojo/artifact');
869
+ const result = await executeTool('dojo_read_artifact', body);
870
+ sendJson(res, result.status === 'error' ? 400 : 200, result);
871
+ }
872
+ catch (error) {
873
+ const message = error instanceof Error ? error.message : String(error);
874
+ sendJson(res, 400, wrapError(message, 'PARSE_ERROR'));
875
+ }
876
+ return;
877
+ }
878
+ // POST /dojo/bench - Run benchmark on experiment
879
+ if (path === '/dojo/bench' && req.method === 'POST') {
880
+ try {
881
+ const body = await parseBody(req);
882
+ log('HTTP: /dojo/bench');
883
+ const result = await executeTool('dojo_bench', body);
884
+ sendJson(res, result.status === 'error' ? 400 : 200, result);
885
+ }
886
+ catch (error) {
887
+ const message = error instanceof Error ? error.message : String(error);
888
+ sendJson(res, 400, wrapError(message, 'PARSE_ERROR'));
889
+ }
890
+ return;
891
+ }
892
+ // ========================================================================
893
+ // Benchmark Endpoints (ISS-0014)
894
+ // ========================================================================
895
+ // POST /bench/run - Run a benchmark suite
896
+ if (path === '/bench/run' && req.method === 'POST') {
897
+ try {
898
+ const body = await parseBody(req);
899
+ log('HTTP: /bench/run');
900
+ const result = await executeTool('decibel_bench', body);
901
+ sendJson(res, result.status === 'error' ? 400 : 200, result);
902
+ }
903
+ catch (error) {
904
+ const message = error instanceof Error ? error.message : String(error);
905
+ sendJson(res, 400, wrapError(message, 'PARSE_ERROR'));
906
+ }
907
+ return;
908
+ }
909
+ // POST /bench/compare - Compare two baselines
910
+ if (path === '/bench/compare' && req.method === 'POST') {
911
+ try {
912
+ const body = await parseBody(req);
913
+ log('HTTP: /bench/compare');
914
+ const result = await executeTool('decibel_bench_compare', body);
915
+ sendJson(res, result.status === 'error' ? 400 : 200, result);
916
+ }
917
+ catch (error) {
918
+ const message = error instanceof Error ? error.message : String(error);
919
+ sendJson(res, 400, wrapError(message, 'PARSE_ERROR'));
920
+ }
921
+ return;
922
+ }
923
+ // ========================================================================
924
+ // Context Pack Endpoints (ADR-002)
925
+ // ========================================================================
926
+ // POST /context/refresh - Compile full context pack
927
+ if (path === '/context/refresh' && req.method === 'POST') {
928
+ try {
929
+ const body = await parseBody(req);
930
+ log('HTTP: /context/refresh');
931
+ const result = await executeTool('decibel_context_refresh', body);
932
+ sendJson(res, result.status === 'error' ? 400 : 200, result);
933
+ }
934
+ catch (error) {
935
+ const message = error instanceof Error ? error.message : String(error);
936
+ sendJson(res, 400, wrapError(message, 'PARSE_ERROR'));
937
+ }
938
+ return;
939
+ }
940
+ // POST /context/pin - Pin a fact
941
+ if (path === '/context/pin' && req.method === 'POST') {
942
+ try {
943
+ const body = await parseBody(req);
944
+ log('HTTP: /context/pin');
945
+ const result = await executeTool('decibel_context_pin', body);
946
+ sendJson(res, result.status === 'error' ? 400 : 200, result);
947
+ }
948
+ catch (error) {
949
+ const message = error instanceof Error ? error.message : String(error);
950
+ sendJson(res, 400, wrapError(message, 'PARSE_ERROR'));
951
+ }
952
+ return;
953
+ }
954
+ // POST /context/unpin - Unpin a fact
955
+ if (path === '/context/unpin' && req.method === 'POST') {
956
+ try {
957
+ const body = await parseBody(req);
958
+ log('HTTP: /context/unpin');
959
+ const result = await executeTool('decibel_context_unpin', body);
960
+ sendJson(res, result.status === 'error' ? 400 : 200, result);
961
+ }
962
+ catch (error) {
963
+ const message = error instanceof Error ? error.message : String(error);
964
+ sendJson(res, 400, wrapError(message, 'PARSE_ERROR'));
965
+ }
966
+ return;
967
+ }
968
+ // GET/POST /context/list - List pinned facts
969
+ if (path === '/context/list' && (req.method === 'GET' || req.method === 'POST')) {
970
+ try {
971
+ const body = req.method === 'POST' ? await parseBody(req) : {};
972
+ if (req.method === 'GET') {
973
+ const projectId = url.searchParams.get('project_id');
974
+ if (projectId) {
975
+ body.project_id = projectId;
976
+ }
977
+ }
978
+ log('HTTP: /context/list');
979
+ const result = await executeTool('decibel_context_list', body);
980
+ sendJson(res, result.status === 'error' ? 400 : 200, result);
981
+ }
982
+ catch (error) {
983
+ const message = error instanceof Error ? error.message : String(error);
984
+ sendJson(res, 400, wrapError(message, 'PARSE_ERROR'));
985
+ }
986
+ return;
987
+ }
988
+ // POST /event/append - Append event to journal
989
+ if (path === '/event/append' && req.method === 'POST') {
990
+ try {
991
+ const body = await parseBody(req);
992
+ log('HTTP: /event/append');
993
+ const result = await executeTool('decibel_event_append', body);
994
+ sendJson(res, result.status === 'error' ? 400 : 200, result);
995
+ }
996
+ catch (error) {
997
+ const message = error instanceof Error ? error.message : String(error);
998
+ sendJson(res, 400, wrapError(message, 'PARSE_ERROR'));
999
+ }
1000
+ return;
1001
+ }
1002
+ // GET/POST /event/search - Search events
1003
+ if (path === '/event/search' && (req.method === 'GET' || req.method === 'POST')) {
1004
+ try {
1005
+ const body = req.method === 'POST' ? await parseBody(req) : {};
1006
+ if (req.method === 'GET') {
1007
+ const projectId = url.searchParams.get('project_id');
1008
+ const query = url.searchParams.get('query');
1009
+ const limit = url.searchParams.get('limit');
1010
+ if (projectId) {
1011
+ body.project_id = projectId;
1012
+ }
1013
+ if (query) {
1014
+ body.query = query;
1015
+ }
1016
+ if (limit) {
1017
+ body.limit = parseInt(limit, 10);
1018
+ }
1019
+ }
1020
+ log('HTTP: /event/search');
1021
+ const result = await executeTool('decibel_event_search', body);
1022
+ sendJson(res, result.status === 'error' ? 400 : 200, result);
1023
+ }
1024
+ catch (error) {
1025
+ const message = error instanceof Error ? error.message : String(error);
1026
+ sendJson(res, 400, wrapError(message, 'PARSE_ERROR'));
1027
+ }
1028
+ return;
1029
+ }
1030
+ // POST /artifact/list - List artifacts for a run
1031
+ if (path === '/artifact/list' && req.method === 'POST') {
1032
+ try {
1033
+ const body = await parseBody(req);
1034
+ log('HTTP: /artifact/list');
1035
+ const result = await executeTool('decibel_artifact_list', body);
1036
+ sendJson(res, result.status === 'error' ? 400 : 200, result);
1037
+ }
1038
+ catch (error) {
1039
+ const message = error instanceof Error ? error.message : String(error);
1040
+ sendJson(res, 400, wrapError(message, 'PARSE_ERROR'));
1041
+ }
1042
+ return;
1043
+ }
1044
+ // POST /artifact/read - Read artifact by run_id and name
1045
+ if (path === '/artifact/read' && req.method === 'POST') {
1046
+ try {
1047
+ const body = await parseBody(req);
1048
+ log('HTTP: /artifact/read');
1049
+ const result = await executeTool('decibel_artifact_read', body);
1050
+ sendJson(res, result.status === 'error' ? 400 : 200, result);
1051
+ }
1052
+ catch (error) {
1053
+ const message = error instanceof Error ? error.message : String(error);
1054
+ sendJson(res, 400, wrapError(message, 'PARSE_ERROR'));
1055
+ }
1056
+ return;
1057
+ }
1058
+ // ========================================================================
1059
+ // iOS Mobile App Endpoint
1060
+ // ========================================================================
1061
+ // Helper: Call ML classifier sidecar (optional, graceful fallback)
1062
+ async function classifyWithML(transcript) {
1063
+ try {
1064
+ const controller = new AbortController();
1065
+ const timeout = setTimeout(() => controller.abort(), 1000); // 1s timeout
1066
+ const resp = await fetch('http://127.0.0.1:8790/classify', {
1067
+ method: 'POST',
1068
+ headers: { 'Content-Type': 'application/json' },
1069
+ body: JSON.stringify({ transcript }),
1070
+ signal: controller.signal,
1071
+ });
1072
+ clearTimeout(timeout);
1073
+ if (resp.ok) {
1074
+ return await resp.json();
1075
+ }
1076
+ }
1077
+ catch {
1078
+ // Classifier not running or timed out - that's fine
1079
+ }
1080
+ return null;
1081
+ }
1082
+ // Helper: Log training sample to ML classifier
1083
+ async function logTrainingSample(data) {
1084
+ try {
1085
+ await fetch('http://127.0.0.1:8790/log', {
1086
+ method: 'POST',
1087
+ headers: { 'Content-Type': 'application/json' },
1088
+ body: JSON.stringify(data),
1089
+ });
1090
+ }
1091
+ catch {
1092
+ // Best effort logging
1093
+ }
1094
+ }
1095
+ // POST /api/inbox - Receive voice transcript from iOS app
1096
+ if (path === '/api/inbox' && req.method === 'POST') {
1097
+ try {
1098
+ const body = await parseBody(req);
1099
+ log('HTTP: /api/inbox (iOS)');
1100
+ // Validate required field
1101
+ const transcript = body.transcript;
1102
+ if (!transcript) {
1103
+ sendJson(res, 400, wrapError('Missing "transcript" field', 'MISSING_TRANSCRIPT'));
1104
+ return;
1105
+ }
1106
+ // Build tags array
1107
+ const tags = [];
1108
+ if (body.device)
1109
+ tags.push(`device:${body.device}`);
1110
+ // User's explicit intent (from button tap)
1111
+ // iOS sends as "event_type", also accept "intent" for compatibility
1112
+ const userIntent = (body.event_type || body.intent);
1113
+ // ML classification (optional - graceful fallback if not running)
1114
+ const mlResult = await classifyWithML(transcript);
1115
+ let finalIntent = userIntent;
1116
+ let wasOverridden = false;
1117
+ let mlConfidence = 0;
1118
+ if (mlResult) {
1119
+ mlConfidence = mlResult.confidence;
1120
+ log(`HTTP: ML classified as "${mlResult.intent}" (${(mlResult.confidence * 100).toFixed(0)}%)`);
1121
+ if (userIntent) {
1122
+ // User provided intent - ML can override if confident and disagrees
1123
+ if (mlResult.intent !== userIntent && mlResult.confidence > 0.75) {
1124
+ finalIntent = mlResult.intent;
1125
+ wasOverridden = true;
1126
+ tags.push('ml:overridden');
1127
+ log(`HTTP: ML overriding user intent "${userIntent}" → "${mlResult.intent}"`);
1128
+ }
1129
+ // Log training sample (user label = ground truth)
1130
+ logTrainingSample({
1131
+ transcript,
1132
+ user_label: userIntent,
1133
+ predicted: mlResult.intent,
1134
+ confidence: mlResult.confidence,
1135
+ was_overridden: wasOverridden,
1136
+ });
1137
+ }
1138
+ else {
1139
+ // No user intent - use ML prediction
1140
+ finalIntent = mlResult.intent;
1141
+ tags.push('ml:predicted');
1142
+ }
1143
+ }
1144
+ // Mark as human-labeled if user provided intent
1145
+ if (userIntent) {
1146
+ tags.push('labeled:human');
1147
+ tags.push(`user_intent:${userIntent}`);
1148
+ }
1149
+ // Map iOS payload to VoiceInboxAddInput
1150
+ const voiceInput = {
1151
+ transcript,
1152
+ source: 'mobile_app',
1153
+ project_id: body.project_id,
1154
+ process_immediately: true, // Process on receipt
1155
+ tags: tags.length > 0 ? tags : undefined,
1156
+ // Pass final intent (may be ML-overridden)
1157
+ explicit_intent: finalIntent,
1158
+ };
1159
+ const result = await voiceInboxAdd(voiceInput);
1160
+ sendJson(res, 200, wrapSuccess({
1161
+ inbox_id: result.inbox_id,
1162
+ transcript: result.transcript,
1163
+ intent: result.intent,
1164
+ intent_confidence: result.intent_confidence,
1165
+ inbox_status: result.status,
1166
+ immediate_result: result.immediate_result,
1167
+ // ML metadata
1168
+ labeled: !!userIntent,
1169
+ user_intent: userIntent || null,
1170
+ ml_intent: mlResult?.intent || null,
1171
+ ml_confidence: mlResult ? Math.round(mlResult.confidence * 100) / 100 : null,
1172
+ was_overridden: wasOverridden,
1173
+ }));
1174
+ }
1175
+ catch (error) {
1176
+ const message = error instanceof Error ? error.message : String(error);
1177
+ log(`HTTP: /api/inbox error: ${message}`);
1178
+ sendJson(res, 400, wrapError(message, 'VOICE_INBOX_ERROR'));
1179
+ }
1180
+ return;
1181
+ }
1182
+ // ========================================================================
1183
+ // iOS App API Endpoints (StatusSnapshot compatible)
1184
+ // ========================================================================
1185
+ // GET /api/projects - List registered projects for iOS project picker
1186
+ if (path === '/api/projects' && req.method === 'GET') {
1187
+ try {
1188
+ log('HTTP: /api/projects');
1189
+ const projects = listProjects();
1190
+ sendJson(res, 200, wrapSuccess({
1191
+ projects: projects.map(p => ({
1192
+ id: p.id,
1193
+ name: p.name || p.id,
1194
+ aliases: p.aliases || [],
1195
+ })),
1196
+ }));
1197
+ }
1198
+ catch (error) {
1199
+ const message = error instanceof Error ? error.message : String(error);
1200
+ log(`HTTP: /api/projects error: ${message}`);
1201
+ sendJson(res, 500, wrapError(message, 'PROJECTS_ERROR'));
1202
+ }
1203
+ return;
1204
+ }
1205
+ // GET /api/status - StatusSnapshot for iOS StatusView
1206
+ if (path === '/api/status' && req.method === 'GET') {
1207
+ try {
1208
+ log('HTTP: /api/status');
1209
+ const projects = listProjects();
1210
+ // Check system health by listing each project's data
1211
+ const systemsHealth = {
1212
+ sentinel: { status: 'healthy', message: null },
1213
+ oracle: { status: 'healthy', message: null },
1214
+ dojo: { status: 'healthy', message: null },
1215
+ architect: { status: 'healthy', message: null },
1216
+ };
1217
+ // Build project summaries
1218
+ const projectSummaries = [];
1219
+ for (const project of projects) {
1220
+ try {
1221
+ // Get epic count
1222
+ const epicsResult = await listEpics({ projectId: project.id });
1223
+ const epicCount = isProjectResolutionError(epicsResult)
1224
+ ? 0
1225
+ : epicsResult.epics?.length || 0;
1226
+ // Get open issues count
1227
+ const issuesResult = await listRepoIssues({ projectId: project.id, status: 'open' });
1228
+ const openIssueCount = isProjectResolutionError(issuesResult)
1229
+ ? 0
1230
+ : issuesResult.issues?.length || 0;
1231
+ projectSummaries.push({
1232
+ project_id: project.id,
1233
+ name: project.name || project.id,
1234
+ active_epics: epicCount,
1235
+ open_issues: openIssueCount,
1236
+ last_activity: null, // Would need to scan files for timestamps
1237
+ });
1238
+ }
1239
+ catch {
1240
+ // If we can't get data for a project, still include it with zeros
1241
+ projectSummaries.push({
1242
+ project_id: project.id,
1243
+ name: project.name || project.id,
1244
+ active_epics: 0,
1245
+ open_issues: 0,
1246
+ last_activity: null,
1247
+ });
1248
+ }
1249
+ }
1250
+ const snapshot = {
1251
+ snapshot_id: crypto.randomUUID(),
1252
+ generated_at: new Date().toISOString(),
1253
+ source: {
1254
+ generator: 'mcp-server',
1255
+ version: PKG.version,
1256
+ },
1257
+ systems: systemsHealth,
1258
+ projects: projectSummaries,
1259
+ builds: [],
1260
+ alerts: [],
1261
+ };
1262
+ sendJson(res, 200, wrapSuccess(snapshot));
1263
+ }
1264
+ catch (error) {
1265
+ const message = error instanceof Error ? error.message : String(error);
1266
+ log(`HTTP: /api/status error: ${message}`);
1267
+ sendJson(res, 500, wrapError(message, 'STATUS_ERROR'));
1268
+ }
1269
+ return;
1270
+ }
1271
+ // ========================================================================
1272
+ // Studio API Endpoints (frontend_v0.2 compatible)
1273
+ // ========================================================================
1274
+ // POST /api/generate-flux-kontext-image - Start image generation
1275
+ if (path === '/api/generate-flux-kontext-image' && req.method === 'POST') {
1276
+ try {
1277
+ const body = await parseBody(req);
1278
+ log('HTTP: /api/generate-flux-kontext-image');
1279
+ // Validate required fields
1280
+ if (!body.prompt) {
1281
+ sendJson(res, 400, wrapError('Missing "prompt" field', 'MISSING_PROMPT'));
1282
+ return;
1283
+ }
1284
+ const input = {
1285
+ asset_id: body.asset_id || `asset_${Date.now()}`,
1286
+ user_id: body.user_id || 'anonymous',
1287
+ prompt: body.prompt,
1288
+ input_image: body.input_image,
1289
+ aspect_ratio: body.aspect_ratio || '16:9',
1290
+ model: body.model || 'flux-kontext-pro',
1291
+ };
1292
+ const result = await generateImage(input);
1293
+ sendJson(res, 200, wrapSuccess(result));
1294
+ }
1295
+ catch (error) {
1296
+ const message = error instanceof Error ? error.message : String(error);
1297
+ log(`HTTP: /api/generate-flux-kontext-image error: ${message}`);
1298
+ sendJson(res, 500, wrapError(message, 'GENERATION_ERROR'));
1299
+ }
1300
+ return;
1301
+ }
1302
+ // GET /api/flux-kontext-status/:taskId - Check image generation status
1303
+ if (path.startsWith('/api/flux-kontext-status/') && req.method === 'GET') {
1304
+ try {
1305
+ const taskId = path.replace('/api/flux-kontext-status/', '');
1306
+ log(`HTTP: /api/flux-kontext-status/${taskId}`);
1307
+ if (!taskId) {
1308
+ sendJson(res, 400, wrapError('Missing task ID', 'MISSING_TASK_ID'));
1309
+ return;
1310
+ }
1311
+ const status = getImageStatus(taskId);
1312
+ if (!status) {
1313
+ sendJson(res, 404, wrapError('Task not found', 'TASK_NOT_FOUND'));
1314
+ return;
1315
+ }
1316
+ res.writeHead(200, { 'Content-Type': 'application/json' });
1317
+ res.end(JSON.stringify(status));
1318
+ }
1319
+ catch (error) {
1320
+ const message = error instanceof Error ? error.message : String(error);
1321
+ log(`HTTP: /api/flux-kontext-status error: ${message}`);
1322
+ sendJson(res, 500, wrapError(message, 'STATUS_ERROR'));
1323
+ }
1324
+ return;
1325
+ }
1326
+ // ========================================================================
1327
+ // Meshy 3D Generation Endpoints
1328
+ // ========================================================================
1329
+ // POST /api/meshy/generate - Start 3D generation
1330
+ if (path === '/api/meshy/generate' && req.method === 'POST') {
1331
+ try {
1332
+ const body = await parseBody(req);
1333
+ log('HTTP: /api/meshy/generate');
1334
+ if (!body.mode) {
1335
+ sendJson(res, 400, wrapError('Missing "mode" field', 'MISSING_MODE'));
1336
+ return;
1337
+ }
1338
+ const input = {
1339
+ mode: body.mode,
1340
+ prompt: body.prompt,
1341
+ image_url: body.image_url,
1342
+ image_urls: body.image_urls,
1343
+ preview_task_id: body.preview_task_id,
1344
+ model_input: body.model_input,
1345
+ parameters: body.parameters,
1346
+ asset_id: body.asset_id,
1347
+ user_id: body.user_id,
1348
+ };
1349
+ const result = await meshyGenerate(input);
1350
+ sendJson(res, 200, wrapSuccess(result));
1351
+ }
1352
+ catch (error) {
1353
+ const message = error instanceof Error ? error.message : String(error);
1354
+ log(`HTTP: /api/meshy/generate error: ${message}`);
1355
+ sendJson(res, 500, wrapError(message, 'MESHY_ERROR'));
1356
+ }
1357
+ return;
1358
+ }
1359
+ // GET /api/meshy/status/:taskId - Check 3D generation status
1360
+ if (path.startsWith('/api/meshy/status/') && req.method === 'GET') {
1361
+ try {
1362
+ const taskId = path.replace('/api/meshy/status/', '').split('?')[0];
1363
+ log(`HTTP: /api/meshy/status/${taskId}`);
1364
+ const status = getMeshyStatus(taskId);
1365
+ if (!status) {
1366
+ sendJson(res, 404, wrapError('Task not found', 'TASK_NOT_FOUND'));
1367
+ return;
1368
+ }
1369
+ res.writeHead(200, { 'Content-Type': 'application/json' });
1370
+ res.end(JSON.stringify(status));
1371
+ }
1372
+ catch (error) {
1373
+ const message = error instanceof Error ? error.message : String(error);
1374
+ sendJson(res, 500, wrapError(message, 'STATUS_ERROR'));
1375
+ }
1376
+ return;
1377
+ }
1378
+ // POST /api/meshy/download - Download completed model
1379
+ if (path === '/api/meshy/download' && req.method === 'POST') {
1380
+ try {
1381
+ const body = await parseBody(req);
1382
+ log('HTTP: /api/meshy/download');
1383
+ if (!body.task_id) {
1384
+ sendJson(res, 400, wrapError('Missing "task_id" field', 'MISSING_TASK_ID'));
1385
+ return;
1386
+ }
1387
+ const result = await meshyDownload(body.task_id, body.asset_id || `asset_${Date.now()}`, body.user_id || 'anonymous');
1388
+ sendJson(res, 200, wrapSuccess(result));
1389
+ }
1390
+ catch (error) {
1391
+ const message = error instanceof Error ? error.message : String(error);
1392
+ sendJson(res, 500, wrapError(message, 'DOWNLOAD_ERROR'));
1393
+ }
1394
+ return;
1395
+ }
1396
+ // ========================================================================
1397
+ // Tripo 3D Generation Endpoints
1398
+ // ========================================================================
1399
+ // POST /api/tripo/generate - Start Tripo 3D generation
1400
+ if (path === '/api/tripo/generate' && req.method === 'POST') {
1401
+ try {
1402
+ const body = await parseBody(req);
1403
+ log('HTTP: /api/tripo/generate');
1404
+ if (!body.type) {
1405
+ sendJson(res, 400, wrapError('Missing "type" field', 'MISSING_TYPE'));
1406
+ return;
1407
+ }
1408
+ const input = {
1409
+ type: body.type,
1410
+ prompt: body.prompt,
1411
+ image_url: body.image_url,
1412
+ image_urls: body.image_urls,
1413
+ parameters: body.parameters,
1414
+ asset_id: body.asset_id,
1415
+ user_id: body.user_id,
1416
+ };
1417
+ const result = await tripoGenerate(input);
1418
+ sendJson(res, 200, wrapSuccess(result));
1419
+ }
1420
+ catch (error) {
1421
+ const message = error instanceof Error ? error.message : String(error);
1422
+ log(`HTTP: /api/tripo/generate error: ${message}`);
1423
+ sendJson(res, 500, wrapError(message, 'TRIPO_ERROR'));
1424
+ }
1425
+ return;
1426
+ }
1427
+ // GET /api/tripo/task/:taskId - Check Tripo task status
1428
+ if (path.startsWith('/api/tripo/task/') && req.method === 'GET') {
1429
+ try {
1430
+ const taskId = path.replace('/api/tripo/task/', '');
1431
+ log(`HTTP: /api/tripo/task/${taskId}`);
1432
+ const status = getTripoStatus(taskId);
1433
+ if (!status) {
1434
+ sendJson(res, 404, wrapError('Task not found', 'TASK_NOT_FOUND'));
1435
+ return;
1436
+ }
1437
+ res.writeHead(200, { 'Content-Type': 'application/json' });
1438
+ res.end(JSON.stringify(status));
1439
+ }
1440
+ catch (error) {
1441
+ const message = error instanceof Error ? error.message : String(error);
1442
+ sendJson(res, 500, wrapError(message, 'STATUS_ERROR'));
1443
+ }
1444
+ return;
1445
+ }
1446
+ // POST /api/tripo/download/:taskId - Download Tripo model
1447
+ if (path.startsWith('/api/tripo/download/') && req.method === 'POST') {
1448
+ try {
1449
+ const taskId = path.replace('/api/tripo/download/', '');
1450
+ const body = await parseBody(req);
1451
+ log(`HTTP: /api/tripo/download/${taskId}`);
1452
+ const result = await tripoDownload(taskId, body.asset_id || `asset_${Date.now()}`, body.user_id || 'anonymous');
1453
+ sendJson(res, 200, wrapSuccess(result));
1454
+ }
1455
+ catch (error) {
1456
+ const message = error instanceof Error ? error.message : String(error);
1457
+ sendJson(res, 500, wrapError(message, 'DOWNLOAD_ERROR'));
1458
+ }
1459
+ return;
1460
+ }
1461
+ // ========================================================================
1462
+ // Kling Video Generation Endpoints
1463
+ // ========================================================================
1464
+ // POST /api/generate-kling-video - Image to video
1465
+ if (path === '/api/generate-kling-video' && req.method === 'POST') {
1466
+ try {
1467
+ const body = await parseBody(req);
1468
+ log('HTTP: /api/generate-kling-video');
1469
+ if (!body.image_url || !body.prompt) {
1470
+ sendJson(res, 400, wrapError('Missing "image_url" or "prompt" field', 'MISSING_FIELDS'));
1471
+ return;
1472
+ }
1473
+ const input = {
1474
+ asset_id: body.asset_id || `asset_${Date.now()}`,
1475
+ image_url: body.image_url,
1476
+ prompt: body.prompt,
1477
+ negative_prompt: body.negative_prompt,
1478
+ duration: body.duration || 5,
1479
+ aspect_ratio: body.aspect_ratio || '16:9',
1480
+ cfg_scale: body.cfg_scale,
1481
+ seed: body.seed,
1482
+ user_id: body.user_id,
1483
+ model: body.model,
1484
+ sound: body.sound,
1485
+ };
1486
+ const result = await klingGenerateVideo(input);
1487
+ sendJson(res, 200, wrapSuccess(result));
1488
+ }
1489
+ catch (error) {
1490
+ const message = error instanceof Error ? error.message : String(error);
1491
+ log(`HTTP: /api/generate-kling-video error: ${message}`);
1492
+ sendJson(res, 500, wrapError(message, 'KLING_ERROR'));
1493
+ }
1494
+ return;
1495
+ }
1496
+ // POST /api/generate-kling-text-video - Text to video
1497
+ if (path === '/api/generate-kling-text-video' && req.method === 'POST') {
1498
+ try {
1499
+ const body = await parseBody(req);
1500
+ log('HTTP: /api/generate-kling-text-video');
1501
+ if (!body.prompt) {
1502
+ sendJson(res, 400, wrapError('Missing "prompt" field', 'MISSING_PROMPT'));
1503
+ return;
1504
+ }
1505
+ const input = {
1506
+ asset_id: body.asset_id || `asset_${Date.now()}`,
1507
+ prompt: body.prompt,
1508
+ negative_prompt: body.negative_prompt,
1509
+ duration: body.duration || 5,
1510
+ aspect_ratio: body.aspect_ratio || '16:9',
1511
+ cfg_scale: body.cfg_scale,
1512
+ user_id: body.user_id,
1513
+ model: body.model,
1514
+ sound: body.sound,
1515
+ };
1516
+ const result = await klingGenerateTextVideo(input);
1517
+ sendJson(res, 200, wrapSuccess(result));
1518
+ }
1519
+ catch (error) {
1520
+ const message = error instanceof Error ? error.message : String(error);
1521
+ log(`HTTP: /api/generate-kling-text-video error: ${message}`);
1522
+ sendJson(res, 500, wrapError(message, 'KLING_ERROR'));
1523
+ }
1524
+ return;
1525
+ }
1526
+ // POST /api/generate-kling-avatar - Avatar/lip-sync video
1527
+ if (path === '/api/generate-kling-avatar' && req.method === 'POST') {
1528
+ try {
1529
+ const body = await parseBody(req);
1530
+ log('HTTP: /api/generate-kling-avatar');
1531
+ if (!body.image_url || !body.audio_url) {
1532
+ sendJson(res, 400, wrapError('Missing "image_url" or "audio_url" field', 'MISSING_FIELDS'));
1533
+ return;
1534
+ }
1535
+ const input = {
1536
+ asset_id: body.asset_id || `asset_${Date.now()}`,
1537
+ image_url: body.image_url,
1538
+ audio_url: body.audio_url,
1539
+ prompt: body.prompt,
1540
+ user_id: body.user_id,
1541
+ model: body.model,
1542
+ };
1543
+ const result = await klingGenerateAvatar(input);
1544
+ sendJson(res, 200, wrapSuccess(result));
1545
+ }
1546
+ catch (error) {
1547
+ const message = error instanceof Error ? error.message : String(error);
1548
+ log(`HTTP: /api/generate-kling-avatar error: ${message}`);
1549
+ sendJson(res, 500, wrapError(message, 'KLING_ERROR'));
1550
+ }
1551
+ return;
1552
+ }
1553
+ // GET /api/kling-video-status/:taskId - Check video generation status
1554
+ if (path.startsWith('/api/kling-video-status/') && req.method === 'GET') {
1555
+ try {
1556
+ const taskId = path.replace('/api/kling-video-status/', '');
1557
+ log(`HTTP: /api/kling-video-status/${taskId}`);
1558
+ const status = getKlingStatus(taskId);
1559
+ if (!status) {
1560
+ sendJson(res, 404, wrapError('Task not found', 'TASK_NOT_FOUND'));
1561
+ return;
1562
+ }
1563
+ res.writeHead(200, { 'Content-Type': 'application/json' });
1564
+ res.end(JSON.stringify(status));
1565
+ }
1566
+ catch (error) {
1567
+ const message = error instanceof Error ? error.message : String(error);
1568
+ sendJson(res, 500, wrapError(message, 'STATUS_ERROR'));
1569
+ }
1570
+ return;
1571
+ }
1572
+ // GET /api/studio/tasks - List all tasks (debug endpoint)
1573
+ if (path === '/api/studio/tasks' && req.method === 'GET') {
1574
+ try {
1575
+ log('HTTP: /api/studio/tasks');
1576
+ const tasks = listTasks();
1577
+ sendJson(res, 200, wrapSuccess({ tasks, count: tasks.length }));
1578
+ }
1579
+ catch (error) {
1580
+ const message = error instanceof Error ? error.message : String(error);
1581
+ sendJson(res, 500, wrapError(message, 'LIST_ERROR'));
1582
+ }
1583
+ return;
1584
+ }
1585
+ // ========================================================================
1586
+ // Full MCP Protocol Endpoint
1587
+ // ========================================================================
1588
+ // (b) MCP endpoint - supports GET, POST, DELETE via StreamableHTTPServerTransport
1589
+ // Handle at /mcp, /sse, /sse/ (ChatGPT uses trailing slash), and root / for compatibility
1590
+ if (path === '/mcp' || path === '/sse' || path === '/sse/' || (path === '/' && (req.method === 'POST' || req.method === 'DELETE'))) {
1591
+ try {
1592
+ // Track SSE connections for keepalive (GET requests establish SSE streams)
1593
+ if (req.method === 'GET') {
1594
+ activeSseConnections.add(res);
1595
+ log(`HTTP: SSE stream opened via GET ${path} (${activeSseConnections.size} active) - keepalive enabled`);
1596
+ // Clean up when connection closes
1597
+ res.on('close', () => {
1598
+ activeSseConnections.delete(res);
1599
+ log(`HTTP: SSE stream closed (${activeSseConnections.size} active)`);
1600
+ });
1601
+ res.on('error', (err) => {
1602
+ activeSseConnections.delete(res);
1603
+ log(`HTTP: SSE stream error: ${err.message}`);
1604
+ });
1605
+ }
1606
+ else if (req.method === 'POST') {
1607
+ log(`HTTP: StreamableHTTP request via POST ${path} - no keepalive needed`);
1608
+ }
1609
+ await transport.handleRequest(req, res);
1610
+ }
1611
+ catch (error) {
1612
+ const message = error instanceof Error ? error.message : String(error);
1613
+ log(`HTTP: Error handling MCP request: ${message}`);
1614
+ // Remove from active connections on error
1615
+ activeSseConnections.delete(res);
1616
+ if (!res.writableEnded) {
1617
+ res.writeHead(500, { 'Content-Type': 'application/json' });
1618
+ res.end(JSON.stringify({ error: message }));
1619
+ }
1620
+ }
1621
+ return;
1622
+ }
1623
+ // 404 for all other paths
1624
+ res.writeHead(404, { 'Content-Type': 'application/json' });
1625
+ res.end(JSON.stringify({ error: 'Not found' }));
1626
+ });
1627
+ // Configure HTTP server timeouts to prevent premature connection drops
1628
+ httpServer.keepAliveTimeout = timeoutMs;
1629
+ httpServer.headersTimeout = timeoutMs + 1000; // Slightly longer than keepAliveTimeout
1630
+ httpServer.listen(port, host, () => {
1631
+ log(`HTTP Server listening on http://${host}:${port}`);
1632
+ console.log(`
1633
+ ╔══════════════════════════════════════════════════════════════╗
1634
+ ║ Decibel MCP Server - HTTP Mode v${PKG.version}${' '.repeat(Math.max(0, 24 - PKG.version.length))}║
1635
+ ╠══════════════════════════════════════════════════════════════╣
1636
+ ║ Endpoints: ║
1637
+ ║ GET /health Health check ║
1638
+ ║ GET /tools List tools ║
1639
+ ║ POST /call Execute tool (generic) ║
1640
+ ║ POST /batch Batch dispatch (parallel) ║
1641
+ ║ GET /events Dispatch event log (query) ║
1642
+ ║ POST /dojo/wish Add wish ║
1643
+ ║ POST /dojo/propose Create proposal ║
1644
+ ║ POST /dojo/scaffold Scaffold experiment ║
1645
+ ║ POST /dojo/run Run experiment ║
1646
+ ║ POST /dojo/results Get results ║
1647
+ ║ POST /dojo/artifact Read artifact file ║
1648
+ ║ GET /dojo/list List all ║
1649
+ ║ POST /mcp Full MCP protocol ║
1650
+ ╠══════════════════════════════════════════════════════════════╣
1651
+ ║ Base URL: http://${host}:${port}${' '.repeat(Math.max(0, 40 - port.toString().length - host.length))}║
1652
+ ${authToken ? '║ Auth: Bearer token required ║' : '║ Auth: None (use --auth-token for security) ║'}
1653
+ ╠══════════════════════════════════════════════════════════════╣
1654
+ ║ SSE Settings: ║
1655
+ ║ Keepalive: ${sseKeepaliveMs}ms${' '.repeat(Math.max(0, 43 - sseKeepaliveMs.toString().length))}║
1656
+ ║ Timeout: ${timeoutMs}ms${' '.repeat(Math.max(0, 43 - timeoutMs.toString().length))}║
1657
+ ║ Retry: ${retryIntervalMs}ms${' '.repeat(Math.max(0, 43 - retryIntervalMs.toString().length))}║
1658
+ ╠══════════════════════════════════════════════════════════════╣
1659
+ ║ Response format: {"status": "executed"|"error", ...} ║
1660
+ ╚══════════════════════════════════════════════════════════════╝
1661
+ `);
1662
+ });
1663
+ // Return lifecycle handle for TransportAdapter.stop()
1664
+ return {
1665
+ async stop() {
1666
+ clearInterval(keepaliveInterval);
1667
+ clearInterval(rateLimiterCleanup);
1668
+ // Stop accepting new connections
1669
+ httpServer.close(() => { });
1670
+ // Wait for in-flight requests to drain (max 10s)
1671
+ if (activeRequests.size > 0) {
1672
+ log(`HTTP: Waiting for ${activeRequests.size} in-flight request(s) to drain...`);
1673
+ const drainStart = Date.now();
1674
+ while (activeRequests.size > 0 && Date.now() - drainStart < 10_000) {
1675
+ await new Promise(r => setTimeout(r, 100));
1676
+ }
1677
+ if (activeRequests.size > 0) {
1678
+ log(`HTTP: ${activeRequests.size} request(s) still active after 10s drain timeout`);
1679
+ }
1680
+ }
1681
+ // Close SSE connections
1682
+ for (const conn of activeSseConnections) {
1683
+ try {
1684
+ if (!conn.writableEnded)
1685
+ conn.end();
1686
+ }
1687
+ catch { /* ignore */ }
1688
+ }
1689
+ activeSseConnections.clear();
1690
+ // Final close
1691
+ await new Promise((resolve) => {
1692
+ // Server may already be closed from above — handle gracefully
1693
+ httpServer.close(() => resolve());
1694
+ // If already closed, resolve immediately
1695
+ setTimeout(resolve, 100);
1696
+ });
1697
+ log('HTTP server stopped');
1698
+ },
1699
+ };
1700
+ }
1701
+ /**
1702
+ * Parse command line arguments for HTTP mode
1703
+ */
1704
+ export function parseHttpArgs(args) {
1705
+ const httpMode = args.includes('--http');
1706
+ const portIndex = args.indexOf('--port');
1707
+ // Render sets PORT env var - use it if available
1708
+ const defaultPort = process.env.PORT ? parseInt(process.env.PORT, 10) : 8787;
1709
+ const port = portIndex !== -1 ? parseInt(args[portIndex + 1], 10) : defaultPort;
1710
+ const authIndex = args.indexOf('--auth-token');
1711
+ const authToken = authIndex !== -1 ? args[authIndex + 1] : undefined;
1712
+ const hostIndex = args.indexOf('--host');
1713
+ const host = hostIndex !== -1 ? args[hostIndex + 1] : '0.0.0.0';
1714
+ // SSE/Connection tuning arguments
1715
+ const keepaliveIndex = args.indexOf('--sse-keepalive');
1716
+ const sseKeepaliveMs = keepaliveIndex !== -1 ? parseInt(args[keepaliveIndex + 1], 10) : undefined;
1717
+ const timeoutIndex = args.indexOf('--timeout');
1718
+ const timeoutMs = timeoutIndex !== -1 ? parseInt(args[timeoutIndex + 1], 10) : undefined;
1719
+ const retryIndex = args.indexOf('--sse-retry');
1720
+ const retryIntervalMs = retryIndex !== -1 ? parseInt(args[retryIndex + 1], 10) : undefined;
1721
+ return { httpMode, port, authToken, host, sseKeepaliveMs, timeoutMs, retryIntervalMs };
1722
+ }
1723
+ //# sourceMappingURL=httpServer.js.map