@pugi/cli 0.1.0-beta.8 → 0.1.0-beta.87

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 (402) hide show
  1. package/CHANGELOG.md +96 -0
  2. package/THIRD_PARTY_NOTICES.md +40 -0
  3. package/assets/pugi-prozr2-mascot.ansi +9 -0
  4. package/bin/run.js +33 -1
  5. package/dist/commands/deploy.js +40 -40
  6. package/dist/commands/flatten.js +191 -0
  7. package/dist/commands/jobs-watch.js +201 -0
  8. package/dist/commands/jobs.js +42 -27
  9. package/dist/commands/smoke.js +133 -0
  10. package/dist/core/agent-progress/cleanup.js +134 -0
  11. package/dist/core/agent-progress/schema.js +144 -0
  12. package/dist/core/agent-progress/writer.js +101 -0
  13. package/dist/core/agents/adaptive-router.js +330 -0
  14. package/dist/core/agents/query-decomposer.js +297 -0
  15. package/dist/core/agents/registry.js +2 -2
  16. package/dist/core/approvals/shortcut-resolver.js +98 -0
  17. package/dist/core/artifact-chain/dispatcher.js +148 -0
  18. package/dist/core/artifact-chain/exporter.js +164 -0
  19. package/dist/core/artifact-chain/state.js +243 -0
  20. package/dist/core/artifact-chain/steps.js +169 -0
  21. package/dist/core/ask-user/question.js +92 -0
  22. package/dist/core/audit/audit-trail.js +275 -0
  23. package/dist/core/auth/ensure-authenticated.js +129 -0
  24. package/dist/core/auth/env-provider.js +238 -0
  25. package/dist/core/auto-open-browser.js +4 -4
  26. package/dist/core/auto-update/channels.js +122 -0
  27. package/dist/core/auto-update/checker.js +241 -0
  28. package/dist/core/auto-update/state.js +235 -0
  29. package/dist/core/bare-mode/index.js +107 -0
  30. package/dist/core/bash/redirect.js +281 -0
  31. package/dist/core/bash-classifier.js +436 -40
  32. package/dist/core/checkpoint/resumer.js +149 -0
  33. package/dist/core/checkpoint/rewinder.js +291 -0
  34. package/dist/core/checkpoints/shadow-git.js +670 -0
  35. package/dist/core/citations/parser.js +109 -0
  36. package/dist/core/classifier/yolo-classifier.js +88 -0
  37. package/dist/core/codegraph/decision-store.js +248 -0
  38. package/dist/core/codegraph/detect-repo.js +459 -0
  39. package/dist/core/codegraph/install.js +134 -0
  40. package/dist/core/codegraph/offer-hook.js +220 -0
  41. package/dist/core/compact/auto-trigger.js +96 -0
  42. package/dist/core/compact/buffer-rewriter.js +115 -0
  43. package/dist/core/compact/summarizer.js +208 -0
  44. package/dist/core/compact/token-counter.js +108 -0
  45. package/dist/core/consensus/anvil-fanout.js +25 -25
  46. package/dist/core/consensus/diff-capture.js +121 -12
  47. package/dist/core/consensus/rubric.js +21 -21
  48. package/dist/core/context/builder.js +6 -6
  49. package/dist/core/context/compaction-events.js +8 -8
  50. package/dist/core/context/compaction.js +31 -31
  51. package/dist/core/context/index.js +15 -8
  52. package/dist/core/context/invariants.js +51 -51
  53. package/dist/core/context/markdown-loader.js +28 -10
  54. package/dist/core/context/markdown-traverse.js +255 -0
  55. package/dist/core/context/pugiignore.js +41 -41
  56. package/dist/core/context/repo-skeleton.js +37 -37
  57. package/dist/core/context/tool-eviction.js +55 -0
  58. package/dist/core/context/watcher.js +32 -32
  59. package/dist/core/context/working-set.js +23 -23
  60. package/dist/core/coordinator/agent-tools.js +77 -0
  61. package/dist/core/coordinator/agent-toolset.js +65 -0
  62. package/dist/core/coordinator/fsm.js +73 -0
  63. package/dist/core/coordinator/mode-fsm.js +70 -0
  64. package/dist/core/cost/rate-card.js +129 -0
  65. package/dist/core/cost/tracker.js +221 -0
  66. package/dist/core/credentials.js +12 -12
  67. package/dist/core/cron/scheduler.js +138 -0
  68. package/dist/core/denial-tracking/index.js +8 -0
  69. package/dist/core/denial-tracking/state.js +264 -0
  70. package/dist/core/diagnostics/probe-runner.js +93 -0
  71. package/dist/core/diagnostics/probes/api.js +46 -0
  72. package/dist/core/diagnostics/probes/auth.js +93 -0
  73. package/dist/core/diagnostics/probes/bare-mode.js +42 -0
  74. package/dist/core/diagnostics/probes/cli-version.js +127 -0
  75. package/dist/core/diagnostics/probes/config.js +72 -0
  76. package/dist/core/diagnostics/probes/denial-tracking.js +57 -0
  77. package/dist/core/diagnostics/probes/disk.js +81 -0
  78. package/dist/core/diagnostics/probes/engine-live.js +46 -0
  79. package/dist/core/diagnostics/probes/git.js +65 -0
  80. package/dist/core/diagnostics/probes/hooks.js +118 -0
  81. package/dist/core/diagnostics/probes/mcp.js +75 -0
  82. package/dist/core/diagnostics/probes/node.js +59 -0
  83. package/dist/core/diagnostics/probes/pnpm.js +36 -0
  84. package/dist/core/diagnostics/probes/pugi-md.js +89 -0
  85. package/dist/core/diagnostics/probes/sandbox.js +40 -0
  86. package/dist/core/diagnostics/probes/session.js +74 -0
  87. package/dist/core/diagnostics/probes/status-snapshot.js +488 -0
  88. package/dist/core/diagnostics/probes/workspace.js +63 -0
  89. package/dist/core/diagnostics/types.js +70 -0
  90. package/dist/core/dispatch/cache-cleanup.js +197 -0
  91. package/dist/core/dispatch/cache-handoff.js +295 -0
  92. package/dist/core/edits/apply-patch-layer-e.js +189 -0
  93. package/dist/core/edits/dispatch.js +293 -7
  94. package/dist/core/edits/format-matrix.js +26 -0
  95. package/dist/core/edits/fuzzy-ladder.js +650 -0
  96. package/dist/core/edits/index.js +3 -1
  97. package/dist/core/edits/journal.js +199 -0
  98. package/dist/core/edits/layer-a-apply.js +15 -15
  99. package/dist/core/edits/layer-a-fuzzy-apply.js +198 -0
  100. package/dist/core/edits/layer-b-apply.js +9 -9
  101. package/dist/core/edits/layer-c-apply.js +6 -6
  102. package/dist/core/edits/layer-d-ast.js +557 -14
  103. package/dist/core/edits/marker-parser.js +12 -12
  104. package/dist/core/edits/security-gate.js +27 -27
  105. package/dist/core/edits/verify-hook.js +273 -0
  106. package/dist/core/edits/worktree.js +322 -0
  107. package/dist/core/engine/anvil-client.js +140 -26
  108. package/dist/core/engine/auto-compact.js +179 -0
  109. package/dist/core/engine/budgets.js +186 -0
  110. package/dist/core/engine/context-prefix.js +155 -0
  111. package/dist/core/engine/index.js +1 -1
  112. package/dist/core/engine/intensity.js +158 -0
  113. package/dist/core/engine/intent.js +260 -0
  114. package/dist/core/engine/native-pugi.js +1295 -227
  115. package/dist/core/engine/prompts.js +134 -16
  116. package/dist/core/engine/strip-internal-fields.js +124 -0
  117. package/dist/core/engine/tool-bridge.js +1295 -59
  118. package/dist/core/evaluation/golden-dataset.js +293 -0
  119. package/dist/core/feedback/queue.js +177 -0
  120. package/dist/core/feedback/submitter.js +145 -0
  121. package/dist/core/file-cache.js +113 -1
  122. package/dist/core/flatten/flatten-repo.js +439 -0
  123. package/dist/core/format/osc8-link.js +28 -0
  124. package/dist/core/hook-chains.js +392 -0
  125. package/dist/core/hooks/citation-verify-hook.js +138 -0
  126. package/dist/core/hooks/citation-verify.js +112 -0
  127. package/dist/core/hooks/events.js +44 -0
  128. package/dist/core/hooks/index.js +15 -0
  129. package/dist/core/hooks/registry.js +213 -0
  130. package/dist/core/hooks/runner.js +236 -0
  131. package/dist/core/hooks/v2/event-emitter.js +115 -0
  132. package/dist/core/hooks/v2/executor.js +282 -0
  133. package/dist/core/hooks/v2/index.js +25 -0
  134. package/dist/core/hooks/v2/lifecycle.js +104 -0
  135. package/dist/core/hooks/v2/loader.js +216 -0
  136. package/dist/core/hooks/v2/matcher.js +125 -0
  137. package/dist/core/hooks/v2/trust.js +143 -0
  138. package/dist/core/hooks/v2/types.js +86 -0
  139. package/dist/core/image/renderer.js +71 -0
  140. package/dist/core/init/detector.js +582 -0
  141. package/dist/core/init/template-renderer.js +242 -0
  142. package/dist/core/jobs/registry.js +18 -18
  143. package/dist/core/ledger/results-tsv.js +142 -0
  144. package/dist/core/log-discipline/stdout-redirect.js +51 -0
  145. package/dist/core/lsp/cache.js +105 -0
  146. package/dist/core/lsp/client.js +776 -0
  147. package/dist/core/lsp/language-detect.js +66 -0
  148. package/dist/core/lsp/post-edit-diagnostics.js +171 -0
  149. package/dist/core/lsp/symbol-tools.js +372 -0
  150. package/dist/core/mcp/client.js +97 -28
  151. package/dist/core/mcp/http-server.js +553 -0
  152. package/dist/core/mcp/orchestrator-tools.js +662 -0
  153. package/dist/core/mcp/permission.js +190 -0
  154. package/dist/core/mcp/registry.js +39 -17
  155. package/dist/core/mcp/server-tools.js +219 -0
  156. package/dist/core/mcp/server.js +397 -0
  157. package/dist/core/mcp/trust.js +10 -10
  158. package/dist/core/memory/dual-write.js +416 -0
  159. package/dist/core/memory/passive-extract.js +130 -0
  160. package/dist/core/memory/phase1-kinds.js +20 -0
  161. package/dist/core/memory/secret-scanner.js +304 -0
  162. package/dist/core/memory-sync/queue.js +170 -0
  163. package/dist/core/metrics/extract.js +113 -0
  164. package/dist/core/modes/roo-modes.js +68 -0
  165. package/dist/core/onboarding/ensure-initialized.js +133 -0
  166. package/dist/core/onboarding/marker.js +111 -0
  167. package/dist/core/onboarding/telemetry-state.js +108 -0
  168. package/dist/core/output-style/presets.js +176 -0
  169. package/dist/core/output-style/state.js +185 -0
  170. package/dist/core/path-security.js +287 -5
  171. package/dist/core/permission.js +82 -22
  172. package/dist/core/permissions/auto-classifier.js +124 -0
  173. package/dist/core/permissions/bash-parser.js +371 -0
  174. package/dist/core/permissions/circuit-breaker.js +83 -0
  175. package/dist/core/permissions/constrained-edit.js +91 -0
  176. package/dist/core/permissions/gate.js +278 -0
  177. package/dist/core/permissions/index.js +20 -0
  178. package/dist/core/permissions/mode.js +174 -0
  179. package/dist/core/permissions/network-egress.js +137 -0
  180. package/dist/core/permissions/state.js +241 -0
  181. package/dist/core/permissions/tool-class.js +93 -0
  182. package/dist/core/plan-mode/ui-state.js +51 -0
  183. package/dist/core/plans/plan-artifact.js +721 -0
  184. package/dist/core/policy-limits/etag-store.js +122 -0
  185. package/dist/core/prd-check/parser.js +215 -0
  186. package/dist/core/prd-check/reporter.js +127 -0
  187. package/dist/core/prd-check/session-review.js +557 -0
  188. package/dist/core/prd-check/verifiers.js +223 -0
  189. package/dist/core/prompt-cache/client-cache.js +99 -0
  190. package/dist/core/prompts/assembly.js +29 -0
  191. package/dist/core/prompts/registry.js +364 -0
  192. package/dist/core/pugi-md/cc-compat-rules.js +735 -0
  193. package/dist/core/pugi-md/context-injector.js +76 -0
  194. package/dist/core/pugi-md/walk-up.js +207 -0
  195. package/dist/core/python/uv-installer.js +270 -0
  196. package/dist/core/python/uv-resolver.js +83 -0
  197. package/dist/core/rate-limit/narrator.js +146 -0
  198. package/dist/core/recipes/cli-types.js +20 -0
  199. package/dist/core/recipes/loader.js +103 -0
  200. package/dist/core/recipes/runner.js +345 -0
  201. package/dist/core/recipes/schema.js +587 -0
  202. package/dist/core/release-notes/parser.js +241 -0
  203. package/dist/core/release-notes/state.js +116 -0
  204. package/dist/core/repl/ask.js +37 -37
  205. package/dist/core/repl/cancellation.js +26 -26
  206. package/dist/core/repl/cap-warning.js +4 -4
  207. package/dist/core/repl/clipboard-read.js +11 -11
  208. package/dist/core/repl/dispatch-fsm.js +12 -12
  209. package/dist/core/repl/history-search.js +15 -15
  210. package/dist/core/repl/history.js +28 -18
  211. package/dist/core/repl/kill-ring.js +5 -5
  212. package/dist/core/repl/model-pricing.js +135 -0
  213. package/dist/core/repl/privacy-banner.js +22 -22
  214. package/dist/core/repl/session.js +2157 -214
  215. package/dist/core/repl/slash-commands.js +533 -40
  216. package/dist/core/repl/store/index.js +1 -1
  217. package/dist/core/repl/store/jsonl-log.js +22 -22
  218. package/dist/core/repl/store/lockfile.js +10 -10
  219. package/dist/core/repl/store/session-store.js +136 -107
  220. package/dist/core/repl/store/types.js +15 -15
  221. package/dist/core/repl/store/uuid-v7.js +12 -12
  222. package/dist/core/repl/workspace-context.js +43 -21
  223. package/dist/core/repo-map/build.js +125 -0
  224. package/dist/core/repo-map/cache.js +185 -0
  225. package/dist/core/repo-map/extractor.js +254 -0
  226. package/dist/core/repo-map/formatter.js +145 -0
  227. package/dist/core/repo-map/page-rank.js +105 -0
  228. package/dist/core/repo-map/scanner.js +211 -0
  229. package/dist/core/retry-budget/budget.js +284 -0
  230. package/dist/core/retry-budget/index.js +5 -0
  231. package/dist/core/retry-budget/retry-cap.js +74 -0
  232. package/dist/core/routing/lead-worker.js +43 -0
  233. package/dist/core/routing/pre-flight-estimator.js +108 -0
  234. package/dist/core/runs/run-tree.js +103 -0
  235. package/dist/core/security/injection-scanner.js +367 -0
  236. package/dist/core/security/output-filter.js +418 -0
  237. package/dist/core/session/env-file.js +105 -0
  238. package/dist/core/session/section-budgets.js +140 -0
  239. package/dist/core/session.js +92 -0
  240. package/dist/core/settings.js +286 -5
  241. package/dist/core/share/formatter.js +271 -0
  242. package/dist/core/share/redactor.js +221 -0
  243. package/dist/core/share/uploader.js +267 -0
  244. package/dist/core/skills/defaults.js +457 -0
  245. package/dist/core/skills/loader.js +22 -22
  246. package/dist/core/skills/sources.js +27 -27
  247. package/dist/core/smoke/headless-driver.js +174 -0
  248. package/dist/core/smoke/orchestrator.js +194 -0
  249. package/dist/core/smoke/runner.js +238 -0
  250. package/dist/core/smoke/scenario-parser.js +316 -0
  251. package/dist/core/statusline.js +99 -0
  252. package/dist/core/subagents/dispatcher-real.js +600 -0
  253. package/dist/core/subagents/dispatcher.js +132 -43
  254. package/dist/core/subagents/index.js +19 -6
  255. package/dist/core/subagents/isolation-matrix.js +213 -0
  256. package/dist/core/subagents/spawn.js +19 -4
  257. package/dist/core/telemetry/emitter.js +229 -0
  258. package/dist/core/telemetry/queue.js +251 -0
  259. package/dist/core/theme/context.js +91 -0
  260. package/dist/core/theme/presets.js +228 -0
  261. package/dist/core/theme/state.js +181 -0
  262. package/dist/core/todos/invariant.js +10 -0
  263. package/dist/core/todos/state.js +177 -0
  264. package/dist/core/tool-schema/compressor.js +89 -0
  265. package/dist/core/transport/version-interceptor.js +166 -0
  266. package/dist/core/trust.js +2 -2
  267. package/dist/core/tui/thinking-block.js +64 -0
  268. package/dist/core/vim/keymap.js +288 -0
  269. package/dist/core/vim/state.js +92 -0
  270. package/dist/core/watch-markers/marker-watcher.js +133 -0
  271. package/dist/core/worktree-manager/cleanup.js +123 -0
  272. package/dist/core/worktree-manager/manager.js +303 -0
  273. package/dist/index.js +28 -0
  274. package/dist/runtime/bootstrap.js +190 -0
  275. package/dist/runtime/cli.js +4151 -489
  276. package/dist/runtime/commands/agents.js +30 -30
  277. package/dist/runtime/commands/budget.js +5 -5
  278. package/dist/runtime/commands/cancel.js +231 -0
  279. package/dist/runtime/commands/chain.js +489 -0
  280. package/dist/runtime/commands/codegraph-status.js +227 -0
  281. package/dist/runtime/commands/compact.js +297 -0
  282. package/dist/runtime/commands/config.js +32 -32
  283. package/dist/runtime/commands/cost.js +199 -0
  284. package/dist/runtime/commands/delegate.js +244 -13
  285. package/dist/runtime/commands/dispatch.js +126 -0
  286. package/dist/runtime/commands/doctor.js +579 -0
  287. package/dist/runtime/commands/feedback.js +184 -0
  288. package/dist/runtime/commands/hooks.js +184 -0
  289. package/dist/runtime/commands/init.js +254 -0
  290. package/dist/runtime/commands/lsp.js +368 -0
  291. package/dist/runtime/commands/mcp.js +879 -0
  292. package/dist/runtime/commands/memory.js +582 -0
  293. package/dist/runtime/commands/model.js +237 -0
  294. package/dist/runtime/commands/onboarding.js +275 -0
  295. package/dist/runtime/commands/patch.js +128 -0
  296. package/dist/runtime/commands/permissions.js +112 -0
  297. package/dist/runtime/commands/plan.js +143 -0
  298. package/dist/runtime/commands/prd-check.js +285 -0
  299. package/dist/runtime/commands/privacy.js +17 -17
  300. package/dist/runtime/commands/recipe.js +325 -0
  301. package/dist/runtime/commands/redo-blob-store.js +92 -0
  302. package/dist/runtime/commands/redo.js +361 -0
  303. package/dist/runtime/commands/release-notes.js +229 -0
  304. package/dist/runtime/commands/repo-map.js +95 -0
  305. package/dist/runtime/commands/report.js +299 -0
  306. package/dist/runtime/commands/resume.js +118 -0
  307. package/dist/runtime/commands/review-consensus.js +68 -53
  308. package/dist/runtime/commands/rewind.js +333 -0
  309. package/dist/runtime/commands/roster.js +14 -14
  310. package/dist/runtime/commands/sessions.js +163 -0
  311. package/dist/runtime/commands/share.js +316 -0
  312. package/dist/runtime/commands/skills.js +31 -31
  313. package/dist/runtime/commands/status.js +186 -0
  314. package/dist/runtime/commands/stickers.js +82 -0
  315. package/dist/runtime/commands/style.js +194 -0
  316. package/dist/runtime/commands/theme.js +196 -0
  317. package/dist/runtime/commands/undo.js +54 -22
  318. package/dist/runtime/commands/update.js +289 -0
  319. package/dist/runtime/commands/vim.js +140 -0
  320. package/dist/runtime/commands/worktree.js +177 -0
  321. package/dist/runtime/commands/worktrees.js +155 -0
  322. package/dist/runtime/headless-repl.js +195 -0
  323. package/dist/runtime/headless.js +543 -0
  324. package/dist/runtime/load-hooks-or-exit.js +71 -0
  325. package/dist/runtime/plan-decompose.js +531 -0
  326. package/dist/runtime/update-check.js +28 -28
  327. package/dist/runtime/version.js +65 -0
  328. package/dist/skills/bundled/batch.js +617 -0
  329. package/dist/skills/bundled/index.js +45 -0
  330. package/dist/skills/bundled/loop.js +358 -0
  331. package/dist/skills/bundled/remember.js +383 -0
  332. package/dist/skills/bundled/simplify.js +289 -0
  333. package/dist/skills/bundled/skillify.js +373 -0
  334. package/dist/skills/bundled/stuck.js +558 -0
  335. package/dist/skills/bundled/verify.js +439 -0
  336. package/dist/testing/vcr.js +486 -0
  337. package/dist/tools/agent-tool.js +229 -0
  338. package/dist/tools/apply-patch.js +556 -0
  339. package/dist/tools/ask-user-question.js +222 -0
  340. package/dist/tools/ask-user.js +115 -0
  341. package/dist/tools/bash.js +623 -45
  342. package/dist/tools/brief.js +224 -0
  343. package/dist/tools/enter-worktree.js +250 -0
  344. package/dist/tools/exit-worktree.js +147 -0
  345. package/dist/tools/file-tools.js +161 -44
  346. package/dist/tools/lsp-tools.js +189 -0
  347. package/dist/tools/mcp-tool.js +260 -0
  348. package/dist/tools/multi-edit.js +361 -0
  349. package/dist/tools/powershell.js +268 -0
  350. package/dist/tools/registry.js +85 -0
  351. package/dist/tools/skill-tool.js +96 -0
  352. package/dist/tools/sleep.js +99 -0
  353. package/dist/tools/synthetic-output.js +133 -0
  354. package/dist/tools/tasks.js +208 -0
  355. package/dist/tools/todo-write.js +184 -0
  356. package/dist/tools/verify-plan-execution.js +295 -0
  357. package/dist/tools/web-fetch-injection-scanner.js +207 -0
  358. package/dist/tools/web-fetch.js +195 -10
  359. package/dist/tools/web-search.js +458 -0
  360. package/dist/tui/agent-progress-card.js +111 -0
  361. package/dist/tui/agent-tree.js +11 -1
  362. package/dist/tui/ask-modal.js +14 -14
  363. package/dist/tui/ask-user-question-prompt.js +203 -0
  364. package/dist/tui/compact-banner.js +81 -0
  365. package/dist/tui/conversation-pane.js +85 -11
  366. package/dist/tui/cost-table.js +111 -0
  367. package/dist/tui/device-flow.js +2 -2
  368. package/dist/tui/doctor-table.js +46 -0
  369. package/dist/tui/feedback-prompt.js +156 -0
  370. package/dist/tui/input-box.js +247 -32
  371. package/dist/tui/login-picker.js +3 -3
  372. package/dist/tui/markdown-render.js +6 -6
  373. package/dist/tui/onboarding-wizard.js +240 -0
  374. package/dist/tui/permissions-picker.js +86 -0
  375. package/dist/tui/render.js +35 -0
  376. package/dist/tui/repl-render.js +332 -54
  377. package/dist/tui/repl-splash-art.js +16 -16
  378. package/dist/tui/repl-splash-mascot.js +48 -24
  379. package/dist/tui/repl-splash.js +22 -22
  380. package/dist/tui/repl.js +124 -44
  381. package/dist/tui/slash-palette.js +6 -6
  382. package/dist/tui/splash.js +2 -2
  383. package/dist/tui/status-bar.js +109 -31
  384. package/dist/tui/status-table.js +7 -0
  385. package/dist/tui/stickers-art.js +136 -0
  386. package/dist/tui/style-table.js +28 -0
  387. package/dist/tui/theme-table.js +29 -0
  388. package/dist/tui/thinking-spinner.js +123 -0
  389. package/dist/tui/tool-stream-pane.js +53 -4
  390. package/dist/tui/update-banner.js +27 -2
  391. package/dist/tui/vim-input.js +267 -0
  392. package/dist/tui/welcome-banner.js +107 -0
  393. package/dist/tui/welcome-data.js +293 -0
  394. package/dist/tui/workspace-context.js +2 -2
  395. package/docs/examples/codegraph.mcp.json +10 -0
  396. package/package.json +23 -6
  397. package/test/scenarios/codegen-create-file.scenario.txt +13 -0
  398. package/test/scenarios/compact-force.scenario.txt +11 -0
  399. package/test/scenarios/identity.scenario.txt +11 -0
  400. package/test/scenarios/persona-handoff.scenario.txt +11 -0
  401. package/test/scenarios/walkback.scenario.txt +12 -0
  402. package/dist/core/engine/compaction-hook.js +0 -154
@@ -0,0 +1,553 @@
1
+ import { randomBytes, randomUUID, timingSafeEqual } from 'node:crypto';
2
+ import { createServer } from 'node:http';
3
+ import { EventEmitter } from 'node:events';
4
+ import { MCP_ERROR_CODES, } from './server.js';
5
+ const DEFAULT_LOCALHOST_ORIGINS = Object.freeze([
6
+ 'http://localhost',
7
+ 'http://127.0.0.1',
8
+ 'http://[::1]',
9
+ 'https://localhost',
10
+ 'https://127.0.0.1',
11
+ 'https://[::1]',
12
+ ]);
13
+ const MAX_BODY_BYTES = 1024 * 1024; // 1 MiB
14
+ const MAX_SSE_CLIENTS_DEFAULT = 32;
15
+ /** Header name SSE clients + RPC callers use to scope events. */
16
+ export const PUGI_CLIENT_ID_HEADER = 'x-pugi-client-id';
17
+ /**
18
+ * Start the HTTP+SSE transport. Returns a handle once the listener is
19
+ * bound — the caller can `await` the close hook for graceful shutdown.
20
+ */
21
+ export async function serveHttp(options) {
22
+ const host = options.host ?? '127.0.0.1';
23
+ const log = options.log ?? (() => { });
24
+ const bearerTokenAutoGenerated = options.bearerToken === undefined;
25
+ const bearerToken = options.bearerToken ?? randomBytes(32).toString('hex');
26
+ const sseClients = new Set();
27
+ const corsOrigins = buildCorsOrigins(options.corsOrigins);
28
+ const maxSseClients = options.maxSseClients ?? MAX_SSE_CLIENTS_DEFAULT;
29
+ const tokenBuffer = Buffer.from(bearerToken, 'utf8');
30
+ // Bind the listener FIRST so we can resolve the effective Host header
31
+ // allowlist (the OS-assigned ephemeral port — when port=0 — is only
32
+ // known after listen). The createServer + listen split below preserves
33
+ // that ordering: handlers created here, allowed hosts computed after
34
+ // listening, then attached via the closure.
35
+ let allowedHosts = new Set();
36
+ const httpServer = createServer((req, res) => {
37
+ handleRequest({
38
+ req,
39
+ res,
40
+ mcpServer: options.server,
41
+ tokenBuffer,
42
+ sseClients,
43
+ corsOrigins,
44
+ allowedHosts,
45
+ maxSseClients,
46
+ log,
47
+ }).catch((error) => {
48
+ log('error', `unhandled http error: ${error.message}`);
49
+ if (!res.headersSent) {
50
+ res.statusCode = 500;
51
+ res.setHeader('Content-Type', 'application/json');
52
+ res.end(JSON.stringify({ error: 'internal_error', message: error.message }));
53
+ }
54
+ });
55
+ });
56
+ // Wire server events -> SSE broadcast. The payload may include a
57
+ // `clientId` (string) injected by the request dispatcher; if so we
58
+ // route the event only to the matching SSE client. Untagged events
59
+ // (no clientId) still broadcast — preserves the single-tenant
60
+ // operator workflow that does not bother to send the header.
61
+ const onToolCall = (payload) => {
62
+ routeSse(sseClients, 'tool_call', { name: payload.name, args: payload.args }, payload.clientId);
63
+ };
64
+ const onToolResult = (payload) => {
65
+ routeSse(sseClients, 'tool_result', { name: payload.name, ok: payload.ok, summary: payload.summary }, payload.clientId);
66
+ };
67
+ options.server.events.on('tool_call', onToolCall);
68
+ options.server.events.on('tool_result', onToolResult);
69
+ // Heartbeat — keep proxies + browser readers alive. 15s matches the
70
+ // admin-api SSE keepalive interval; same intermediary defenses (CDNs
71
+ // that drop quiet streams at ~30s). Heartbeats are untagged — every
72
+ // listener gets them.
73
+ const heartbeatTimer = setInterval(() => {
74
+ routeSse(sseClients, 'heartbeat', { ts: new Date().toISOString() }, undefined);
75
+ }, 15_000);
76
+ // Don't block process exit on the timer.
77
+ if (typeof heartbeatTimer.unref === 'function')
78
+ heartbeatTimer.unref();
79
+ // Bind the listener.
80
+ await new Promise((resolve, reject) => {
81
+ const onError = (error) => {
82
+ httpServer.off('listening', onListening);
83
+ reject(error);
84
+ };
85
+ const onListening = () => {
86
+ httpServer.off('error', onError);
87
+ resolve();
88
+ };
89
+ httpServer.once('error', onError);
90
+ httpServer.once('listening', onListening);
91
+ httpServer.listen(options.port, host);
92
+ });
93
+ // Compute the effective bound port + Host allowlist. `address()`
94
+ // returns the OS-assigned port when caller passed 0.
95
+ const address = httpServer.address();
96
+ const effectivePort = address && typeof address === 'object' ? address.port : options.port;
97
+ allowedHosts = buildAllowedHosts(host, effectivePort, options.allowedHosts);
98
+ const url = `http://${host}:${effectivePort}`;
99
+ log('info', `pugi mcp http listening at ${url}`);
100
+ const close = async () => {
101
+ clearInterval(heartbeatTimer);
102
+ options.server.events.off('tool_call', onToolCall);
103
+ options.server.events.off('tool_result', onToolResult);
104
+ for (const client of sseClients)
105
+ client.close();
106
+ sseClients.clear();
107
+ await new Promise((resolveClose) => httpServer.close(() => resolveClose()));
108
+ };
109
+ if (options.signal) {
110
+ if (options.signal.aborted) {
111
+ await close();
112
+ }
113
+ else {
114
+ options.signal.addEventListener('abort', () => {
115
+ void close();
116
+ }, { once: true });
117
+ }
118
+ }
119
+ return {
120
+ url,
121
+ bearerToken,
122
+ bearerTokenAutoGenerated,
123
+ server: httpServer,
124
+ close,
125
+ };
126
+ }
127
+ async function handleRequest(input) {
128
+ const { req, res, mcpServer, tokenBuffer, sseClients, corsOrigins, allowedHosts, maxSseClients, log } = input;
129
+ // P0 #3 — Host header allowlist defends against DNS rebinding. The
130
+ // attacker page rebinds attacker.com → 127.0.0.1 (TTL=0), then issues
131
+ // a same-origin (`Host: attacker.com`) fetch. CORS does not gate
132
+ // same-origin requests; only a Host check stops it.
133
+ const hostHeader = headerString(req, 'host');
134
+ if (!hostHeader || !allowedHosts.has(hostHeader.toLowerCase())) {
135
+ // 421 Misdirected Request — the canonical HTTP code for "this server
136
+ // does not answer for that Host". Matches the Ollama / Jupyter
137
+ // mitigation choices for the same attack class.
138
+ res.statusCode = 421;
139
+ res.setHeader('Content-Type', 'application/json; charset=utf-8');
140
+ res.end(JSON.stringify({
141
+ error: 'host_not_allowed',
142
+ message: `pugi mcp serve: Host header "${hostHeader ?? '<missing>'}" is not in the allowlist`,
143
+ }));
144
+ return;
145
+ }
146
+ applyCorsHeaders(req, res, corsOrigins);
147
+ // Pre-flight.
148
+ if (req.method === 'OPTIONS') {
149
+ res.statusCode = 204;
150
+ res.end();
151
+ return;
152
+ }
153
+ const url = req.url ?? '/';
154
+ // Strip query string for routing — endpoints are query-agnostic today.
155
+ const [pathnameRaw, queryString = ''] = url.split('?');
156
+ const pathname = pathnameRaw ?? '/';
157
+ if (pathname === '/mcp/v1/health' && req.method === 'GET') {
158
+ sendJson(res, 200, { ok: true, service: 'pugi-mcp', version: '0.1.0' });
159
+ return;
160
+ }
161
+ // Auth gate for everything else.
162
+ if (!checkAuth(req, tokenBuffer)) {
163
+ sendJson(res, 401, {
164
+ error: 'auth_required',
165
+ message: 'missing or invalid Authorization: Bearer <token>',
166
+ });
167
+ return;
168
+ }
169
+ if (pathname === '/mcp/v1/events' && req.method === 'GET') {
170
+ handleSse(req, res, sseClients, maxSseClients, queryString);
171
+ return;
172
+ }
173
+ if (req.method !== 'POST') {
174
+ sendJson(res, 405, { error: 'method_not_allowed', message: `use POST for ${pathname}` });
175
+ return;
176
+ }
177
+ // P1 #4 — early Content-Length cap. Reject the body before we read a
178
+ // single byte so a 4 GB POST never reaches `readJsonBody`. Some
179
+ // clients (curl --data-binary @big.bin) omit Content-Length and chunk-
180
+ // encode; for those `readJsonBody` enforces the same cap mid-stream.
181
+ const declaredLength = Number.parseInt(headerString(req, 'content-length') ?? '', 10);
182
+ if (Number.isFinite(declaredLength) && declaredLength > MAX_BODY_BYTES) {
183
+ res.statusCode = 413;
184
+ res.setHeader('Content-Type', 'application/json; charset=utf-8');
185
+ res.end(JSON.stringify({
186
+ error: 'payload_too_large',
187
+ message: `request body declared ${declaredLength} bytes; cap is ${MAX_BODY_BYTES}`,
188
+ }));
189
+ return;
190
+ }
191
+ let body;
192
+ try {
193
+ body = await readJsonBody(req);
194
+ }
195
+ catch (error) {
196
+ sendJson(res, 400, {
197
+ error: 'invalid_json',
198
+ message: error instanceof Error ? error.message : String(error),
199
+ });
200
+ return;
201
+ }
202
+ // Resolve callerId for per-connection SSE scoping. Header beats query
203
+ // string; missing both means "untagged" (broadcast semantics preserved).
204
+ const callerId = resolveCallerId(req, queryString);
205
+ switch (pathname) {
206
+ case '/mcp/v1/initialize':
207
+ await handleRpcShortcut(res, mcpServer, 'initialize', body, callerId);
208
+ // β4 r2 P2 #4 — auto-complete the MCP handshake on behalf of
209
+ // shortcut clients. The MCP wire spec separates `initialize`
210
+ // (capabilities exchange) from `notifications/initialized` (client
211
+ // confirms it is ready), and the server's `requireInitialized`
212
+ // gate refuses `tools/call` until BOTH have fired. The shortcut
213
+ // endpoints abstract over JSON-RPC framing so callers (curl,
214
+ // Postman, ad-hoc fetch from a Worker) never see the second leg —
215
+ // we fire it ourselves so a `POST /initialize` followed by
216
+ // `POST /call` works as the shortcut surface promises. The raw
217
+ // `/rpc` endpoint still requires the explicit notification because
218
+ // its contract is "drive the wire protocol yourself".
219
+ await mcpServer
220
+ .handleMessage({
221
+ jsonrpc: '2.0',
222
+ method: 'notifications/initialized',
223
+ // No `id` — notifications never carry one. The server
224
+ // dispatcher returns null for notifications, so this never
225
+ // produces a response we'd need to drop.
226
+ ...(callerId ? { meta: { clientId: callerId } } : {}),
227
+ })
228
+ .catch((error) => {
229
+ // Best-effort: a notification failure cannot fail the prior
230
+ // /initialize response (already written). Log and continue —
231
+ // the next /call will surface the underlying issue with a
232
+ // clean INVALID_REQUEST.
233
+ log('warn', `auto-initialized notification failed: ${error.message}`);
234
+ });
235
+ return;
236
+ case '/mcp/v1/list':
237
+ await handleRpcShortcut(res, mcpServer, 'tools/list', body ?? {}, callerId);
238
+ return;
239
+ case '/mcp/v1/call':
240
+ await handleRpcShortcut(res, mcpServer, 'tools/call', body, callerId);
241
+ return;
242
+ case '/mcp/v1/rpc':
243
+ await handleRpcPassthrough(res, mcpServer, body, callerId, log);
244
+ return;
245
+ default:
246
+ sendJson(res, 404, { error: 'not_found', message: `unknown endpoint: ${pathname}` });
247
+ return;
248
+ }
249
+ }
250
+ async function handleRpcShortcut(res, mcpServer, method, params, callerId) {
251
+ const request = {
252
+ jsonrpc: '2.0',
253
+ id: 1, // synthetic — the shortcut path never multiplexes
254
+ method,
255
+ ...(params !== undefined ? { params } : {}),
256
+ ...(callerId ? { meta: { clientId: callerId } } : {}),
257
+ };
258
+ const response = await mcpServer.handleMessage(request);
259
+ if (!response) {
260
+ sendJson(res, 204, null);
261
+ return;
262
+ }
263
+ // Map JSON-RPC errors to HTTP status to make the shortcut usable from
264
+ // curl + Postman without parsing the envelope.
265
+ const httpStatus = jsonRpcErrorToHttpStatus(response);
266
+ sendJson(res, httpStatus, response);
267
+ }
268
+ async function handleRpcPassthrough(res, mcpServer, body, callerId, log) {
269
+ if (!body || typeof body !== 'object' || Array.isArray(body)) {
270
+ sendJson(res, 400, {
271
+ jsonrpc: '2.0',
272
+ id: null,
273
+ error: { code: MCP_ERROR_CODES.INVALID_REQUEST, message: 'request body must be a JSON object' },
274
+ });
275
+ return;
276
+ }
277
+ const candidate = body;
278
+ if (candidate.jsonrpc !== '2.0' || typeof candidate.method !== 'string') {
279
+ sendJson(res, 400, {
280
+ jsonrpc: '2.0',
281
+ id: candidate.id ?? null,
282
+ error: {
283
+ code: MCP_ERROR_CODES.INVALID_REQUEST,
284
+ message: 'invalid JSON-RPC envelope: jsonrpc=2.0 + string method required',
285
+ },
286
+ });
287
+ return;
288
+ }
289
+ const request = {
290
+ jsonrpc: '2.0',
291
+ method: candidate.method,
292
+ ...(candidate.id !== undefined ? { id: candidate.id } : {}),
293
+ ...(candidate.params !== undefined ? { params: candidate.params } : {}),
294
+ ...(callerId ? { meta: { clientId: callerId } } : {}),
295
+ };
296
+ try {
297
+ const response = await mcpServer.handleMessage(request);
298
+ if (!response) {
299
+ sendJson(res, 204, null);
300
+ return;
301
+ }
302
+ sendJson(res, jsonRpcErrorToHttpStatus(response), response);
303
+ }
304
+ catch (error) {
305
+ log('error', `rpc passthrough failed: ${error.message}`);
306
+ sendJson(res, 500, {
307
+ jsonrpc: '2.0',
308
+ id: candidate.id ?? null,
309
+ error: { code: MCP_ERROR_CODES.INTERNAL_ERROR, message: error.message },
310
+ });
311
+ }
312
+ }
313
+ function jsonRpcErrorToHttpStatus(response) {
314
+ if (!('error' in response))
315
+ return 200;
316
+ switch (response.error.code) {
317
+ case MCP_ERROR_CODES.METHOD_NOT_FOUND:
318
+ return 404;
319
+ case MCP_ERROR_CODES.INVALID_REQUEST:
320
+ case MCP_ERROR_CODES.INVALID_PARAMS:
321
+ case MCP_ERROR_CODES.PARSE_ERROR:
322
+ return 400;
323
+ case MCP_ERROR_CODES.PERMISSION_REFUSED:
324
+ return 403;
325
+ case MCP_ERROR_CODES.AUTH_REQUIRED:
326
+ return 401;
327
+ default:
328
+ return 500;
329
+ }
330
+ }
331
+ function handleSse(req, res, sseClients, maxSseClients, queryString) {
332
+ // P1 #4 — connection cap. A misbehaving caller cannot accumulate
333
+ // dangling SSE handles indefinitely; the 33rd connection bounces.
334
+ if (sseClients.size >= maxSseClients) {
335
+ res.statusCode = 503;
336
+ res.setHeader('Content-Type', 'application/json; charset=utf-8');
337
+ res.end(JSON.stringify({
338
+ error: 'sse_capacity',
339
+ message: `pugi mcp serve: SSE client cap (${maxSseClients}) reached`,
340
+ }));
341
+ return;
342
+ }
343
+ res.statusCode = 200;
344
+ res.setHeader('Content-Type', 'text/event-stream');
345
+ res.setHeader('Cache-Control', 'no-cache, no-transform');
346
+ res.setHeader('Connection', 'keep-alive');
347
+ const clientId = resolveCallerId(req, queryString);
348
+ // Surface the assigned/observed clientId in a comment frame so the
349
+ // listener can correlate it with subsequent tool-call POSTs.
350
+ res.write(`:ready clientId=${clientId ?? ''}\n\n`);
351
+ const client = {
352
+ res,
353
+ clientId,
354
+ close: () => {
355
+ try {
356
+ res.end();
357
+ }
358
+ catch {
359
+ // already closed
360
+ }
361
+ },
362
+ };
363
+ sseClients.add(client);
364
+ const cleanup = () => {
365
+ sseClients.delete(client);
366
+ };
367
+ req.on('close', cleanup);
368
+ req.on('error', cleanup);
369
+ }
370
+ /**
371
+ * Route an SSE event to the correct subset of clients.
372
+ *
373
+ * - `targetClientId` undefined → broadcast (every connected client).
374
+ * Used for heartbeats AND for tool events that omit the clientId
375
+ * header (single-tenant operator default).
376
+ * - `targetClientId` set → deliver only to clients that opened the
377
+ * stream with the matching clientId. Other listeners (different
378
+ * paired agents) do not see the event. This is the per-connection
379
+ * confidentiality scope (β4 r1 P1 #5).
380
+ */
381
+ function routeSse(sseClients, event, data, targetClientId) {
382
+ const payload = `event: ${event}\ndata: ${JSON.stringify(data)}\n\n`;
383
+ for (const client of sseClients) {
384
+ if (targetClientId !== undefined && client.clientId !== targetClientId) {
385
+ continue;
386
+ }
387
+ try {
388
+ client.res.write(payload);
389
+ }
390
+ catch {
391
+ // Best-effort — the close listener cleans up.
392
+ }
393
+ }
394
+ }
395
+ /* ---------- helpers ---------------------------------------------------- */
396
+ function applyCorsHeaders(req, res, origins) {
397
+ const origin = headerString(req, 'origin');
398
+ // Resolve the request origin against the allowlist. If the request
399
+ // has no Origin header (curl, server-to-server) we skip CORS — the
400
+ // bearer-token gate is the actual auth boundary. We deliberately
401
+ // never emit `Access-Control-Allow-Credentials: true` — no endpoint
402
+ // uses cookies, and the credentialed-fetch hole it created (paired
403
+ // with port-agnostic origins) was the β4 r1 P0 #2 root cause.
404
+ if (origin && originAllowed(origin, origins)) {
405
+ res.setHeader('Access-Control-Allow-Origin', origin);
406
+ res.setHeader('Vary', 'Origin');
407
+ res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
408
+ res.setHeader('Access-Control-Allow-Headers', `Authorization, Content-Type, ${PUGI_CLIENT_ID_HEADER}`);
409
+ res.setHeader('Access-Control-Max-Age', '600');
410
+ }
411
+ }
412
+ function originAllowed(origin, allowlist) {
413
+ // Exact match is the only safe gate. The previous implementation did
414
+ // `origin.startsWith(candidate + ':')` which whitelisted EVERY port on
415
+ // localhost — combined with credentialed fetch (now removed) it
416
+ // created a cross-origin read primitive for any locally-running web
417
+ // server. We now require the operator to add the exact origin
418
+ // (including port) via `corsOrigins`, OR rely on the bare-host
419
+ // localhost defaults (no port → matches the browser's `http://localhost`
420
+ // canonical form).
421
+ return allowlist.has(origin);
422
+ }
423
+ function buildCorsOrigins(extra) {
424
+ const set = new Set(DEFAULT_LOCALHOST_ORIGINS);
425
+ if (!extra)
426
+ return set;
427
+ for (const origin of extra) {
428
+ if (origin === '*') {
429
+ throw new Error('pugi mcp serve --http: wildcard CORS origin "*" is not supported (use a specific origin)');
430
+ }
431
+ set.add(origin);
432
+ }
433
+ return set;
434
+ }
435
+ function buildAllowedHosts(host, port, extra) {
436
+ // Standard local-only allowlist. Lowercase normalised because the
437
+ // Host header is case-insensitive per RFC 7230 §5.4 — we lowercase
438
+ // both sides at compare-time too.
439
+ const set = new Set([
440
+ `127.0.0.1:${port}`,
441
+ `localhost:${port}`,
442
+ `[::1]:${port}`,
443
+ // Bind host may be 0.0.0.0 / non-loopback — still register it so
444
+ // the operator's intentional broader bind keeps working.
445
+ `${host.toLowerCase()}:${port}`,
446
+ ]);
447
+ if (extra) {
448
+ for (const entry of extra)
449
+ set.add(entry.toLowerCase());
450
+ }
451
+ return set;
452
+ }
453
+ function checkAuth(req, tokenBuffer) {
454
+ const header = headerString(req, 'authorization');
455
+ if (!header)
456
+ return false;
457
+ const match = /^Bearer\s+(.+)$/.exec(header.trim());
458
+ if (!match)
459
+ return false;
460
+ const supplied = Buffer.from(match[1] ?? '', 'utf8');
461
+ if (supplied.length !== tokenBuffer.length)
462
+ return false;
463
+ try {
464
+ return timingSafeEqual(supplied, tokenBuffer);
465
+ }
466
+ catch {
467
+ return false;
468
+ }
469
+ }
470
+ function headerString(req, name) {
471
+ const value = req.headers[name];
472
+ if (Array.isArray(value))
473
+ return value[0] ?? null;
474
+ return value ?? null;
475
+ }
476
+ /**
477
+ * Resolve the caller's stable clientId. Header is the canonical channel
478
+ * (clients that POST a tool call can declare it programmatically); the
479
+ * query string is the fallback for SSE GETs because browsers cannot set
480
+ * custom headers on `EventSource`.
481
+ *
482
+ * β4 r2 P2 #5 — GET requests ALWAYS get a stable id (auto-assigned via
483
+ * randomUUID when the caller supplied neither header nor query). Before
484
+ * this fix the `if (!queryString) return undefined` guard short-circuited
485
+ * BEFORE the GET-auto-assign branch, so a bare `GET /mcp/v1/events`
486
+ * (no query string at all) landed in the "untagged broadcast" routing
487
+ * bucket and received events meant for OTHER tagged clients.
488
+ *
489
+ * POSTs that omit it stay untagged on purpose (single-tenant operator
490
+ * default — the dispatcher emits untagged tool events that broadcast).
491
+ */
492
+ function resolveCallerId(req, queryString) {
493
+ const headerValue = headerString(req, PUGI_CLIENT_ID_HEADER);
494
+ if (headerValue && headerValue.trim().length > 0)
495
+ return headerValue.trim();
496
+ if (queryString) {
497
+ const params = new URLSearchParams(queryString);
498
+ const fromQuery = params.get('clientId');
499
+ if (fromQuery && fromQuery.trim().length > 0)
500
+ return fromQuery.trim();
501
+ }
502
+ // β4 r2 P2 #5 — bare GET (no header, no query, OR query without a
503
+ // clientId param) still gets an auto-id so the SSE listener never
504
+ // shares the broadcast bucket with another subscriber. The auto-id
505
+ // is surfaced via the `:ready clientId=<uuid>\n\n` SSE comment in
506
+ // handleSse so the listener can copy it into subsequent POSTs.
507
+ if (req.method === 'GET')
508
+ return randomUUID();
509
+ return undefined;
510
+ }
511
+ async function readJsonBody(req) {
512
+ const chunks = [];
513
+ let total = 0;
514
+ for await (const chunk of req) {
515
+ const buf = typeof chunk === 'string' ? Buffer.from(chunk, 'utf8') : chunk;
516
+ total += buf.length;
517
+ if (total > MAX_BODY_BYTES) {
518
+ throw new Error(`request body exceeds ${MAX_BODY_BYTES} bytes`);
519
+ }
520
+ chunks.push(buf);
521
+ }
522
+ if (chunks.length === 0)
523
+ return undefined;
524
+ const raw = Buffer.concat(chunks).toString('utf8');
525
+ if (raw.trim().length === 0)
526
+ return undefined;
527
+ try {
528
+ return JSON.parse(raw);
529
+ }
530
+ catch (error) {
531
+ throw new Error(`invalid JSON body: ${error.message}`);
532
+ }
533
+ }
534
+ function sendJson(res, status, body) {
535
+ res.statusCode = status;
536
+ res.setHeader('Content-Type', 'application/json; charset=utf-8');
537
+ if (body === null) {
538
+ res.end();
539
+ return;
540
+ }
541
+ res.end(JSON.stringify(body));
542
+ }
543
+ /* ---------- shared emitter for tests ----------------------------------- */
544
+ /**
545
+ * Internal helper exposed for tests: build an in-memory EventEmitter
546
+ * that mirrors the broadcast surface. The real server uses
547
+ * `mcpServer.events` directly; tests that want to drive synthetic
548
+ * events without a full MCP round-trip use this.
549
+ */
550
+ export function createTestEventBus() {
551
+ return new EventEmitter();
552
+ }
553
+ //# sourceMappingURL=http-server.js.map