patchwork-os 0.2.0-alpha.9 → 0.2.0-beta.1

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 (618) hide show
  1. package/README.bridge.md +6 -0
  2. package/README.md +318 -35
  3. package/deploy/bootstrap-new-vps.sh +12 -12
  4. package/deploy/bootstrap-vps.sh +187 -0
  5. package/deploy/deploy-dashboard.sh +174 -0
  6. package/deploy/deploy-landing.sh +136 -0
  7. package/dist/activationMetrics.d.ts +67 -0
  8. package/dist/activationMetrics.js +255 -0
  9. package/dist/activationMetrics.js.map +1 -0
  10. package/dist/activityLog.d.ts +49 -0
  11. package/dist/activityLog.js +78 -0
  12. package/dist/activityLog.js.map +1 -1
  13. package/dist/analyticsAggregator.d.ts +5 -1
  14. package/dist/analyticsAggregator.js +15 -4
  15. package/dist/analyticsAggregator.js.map +1 -1
  16. package/dist/analyticsPrefs.d.ts +11 -0
  17. package/dist/analyticsPrefs.js +33 -0
  18. package/dist/analyticsPrefs.js.map +1 -1
  19. package/dist/approvalHttp.d.ts +49 -2
  20. package/dist/approvalHttp.js +217 -21
  21. package/dist/approvalHttp.js.map +1 -1
  22. package/dist/approvalInsights.d.ts +49 -0
  23. package/dist/approvalInsights.js +97 -0
  24. package/dist/approvalInsights.js.map +1 -0
  25. package/dist/approvalQueue.d.ts +27 -1
  26. package/dist/approvalQueue.js +123 -3
  27. package/dist/approvalQueue.js.map +1 -1
  28. package/dist/approvalSignals.d.ts +124 -0
  29. package/dist/approvalSignals.js +512 -0
  30. package/dist/approvalSignals.js.map +1 -0
  31. package/dist/automation.d.ts +57 -0
  32. package/dist/automation.js +156 -59
  33. package/dist/automation.js.map +1 -1
  34. package/dist/automationSuggestions.d.ts +79 -0
  35. package/dist/automationSuggestions.js +150 -0
  36. package/dist/automationSuggestions.js.map +1 -0
  37. package/dist/bridge.d.ts +3 -0
  38. package/dist/bridge.js +194 -153
  39. package/dist/bridge.js.map +1 -1
  40. package/dist/bridgeToken.js +57 -19
  41. package/dist/bridgeToken.js.map +1 -1
  42. package/dist/ccPermissions.d.ts +15 -0
  43. package/dist/ccPermissions.js +21 -4
  44. package/dist/ccPermissions.js.map +1 -1
  45. package/dist/claudeDriver.d.ts +0 -16
  46. package/dist/claudeDriver.js +93 -36
  47. package/dist/claudeDriver.js.map +1 -1
  48. package/dist/claudeMdPatch.d.ts +9 -3
  49. package/dist/claudeMdPatch.js +79 -13
  50. package/dist/claudeMdPatch.js.map +1 -1
  51. package/dist/claudeOrchestrator.d.ts +13 -1
  52. package/dist/claudeOrchestrator.js +16 -8
  53. package/dist/claudeOrchestrator.js.map +1 -1
  54. package/dist/commands/dashboard.js +1 -1
  55. package/dist/commands/dashboard.js.map +1 -1
  56. package/dist/commands/launchd.d.ts +2 -0
  57. package/dist/commands/launchd.js +94 -0
  58. package/dist/commands/launchd.js.map +1 -0
  59. package/dist/commands/marketplace.d.ts +15 -10
  60. package/dist/commands/marketplace.js +27 -115
  61. package/dist/commands/marketplace.js.map +1 -1
  62. package/dist/commands/patchworkInit.d.ts +8 -0
  63. package/dist/commands/patchworkInit.js +77 -11
  64. package/dist/commands/patchworkInit.js.map +1 -1
  65. package/dist/commands/recipe.d.ts +289 -0
  66. package/dist/commands/recipe.js +1359 -0
  67. package/dist/commands/recipe.js.map +1 -0
  68. package/dist/commands/recipeInstall.d.ts +150 -0
  69. package/dist/commands/recipeInstall.js +647 -0
  70. package/dist/commands/recipeInstall.js.map +1 -0
  71. package/dist/commands/tracesExport.d.ts +83 -0
  72. package/dist/commands/tracesExport.js +269 -0
  73. package/dist/commands/tracesExport.js.map +1 -0
  74. package/dist/commands/tracesImport.d.ts +56 -0
  75. package/dist/commands/tracesImport.js +161 -0
  76. package/dist/commands/tracesImport.js.map +1 -0
  77. package/dist/commitIssueLinkLog.d.ts +8 -0
  78. package/dist/commitIssueLinkLog.js +53 -1
  79. package/dist/commitIssueLinkLog.js.map +1 -1
  80. package/dist/config.d.ts +23 -2
  81. package/dist/config.js +119 -9
  82. package/dist/config.js.map +1 -1
  83. package/dist/connectorRoutes.d.ts +43 -0
  84. package/dist/connectorRoutes.js +1300 -0
  85. package/dist/connectorRoutes.js.map +1 -0
  86. package/dist/connectors/asana.d.ts +198 -0
  87. package/dist/connectors/asana.js +679 -0
  88. package/dist/connectors/asana.js.map +1 -0
  89. package/dist/connectors/baseConnector.d.ts +153 -0
  90. package/dist/connectors/baseConnector.js +336 -0
  91. package/dist/connectors/baseConnector.js.map +1 -0
  92. package/dist/connectors/confluence.d.ts +111 -0
  93. package/dist/connectors/confluence.js +406 -0
  94. package/dist/connectors/confluence.js.map +1 -0
  95. package/dist/connectors/datadog.d.ts +116 -0
  96. package/dist/connectors/datadog.js +385 -0
  97. package/dist/connectors/datadog.js.map +1 -0
  98. package/dist/connectors/discord.d.ts +150 -0
  99. package/dist/connectors/discord.js +543 -0
  100. package/dist/connectors/discord.js.map +1 -0
  101. package/dist/connectors/fixtureLibrary.d.ts +21 -0
  102. package/dist/connectors/fixtureLibrary.js +70 -0
  103. package/dist/connectors/fixtureLibrary.js.map +1 -0
  104. package/dist/connectors/fixtureRecorder.d.ts +1 -0
  105. package/dist/connectors/fixtureRecorder.js +35 -0
  106. package/dist/connectors/fixtureRecorder.js.map +1 -0
  107. package/dist/connectors/github.js +17 -18
  108. package/dist/connectors/github.js.map +1 -1
  109. package/dist/connectors/gitlab.d.ts +180 -0
  110. package/dist/connectors/gitlab.js +582 -0
  111. package/dist/connectors/gitlab.js.map +1 -0
  112. package/dist/connectors/gmail.d.ts +4 -1
  113. package/dist/connectors/gmail.js +149 -27
  114. package/dist/connectors/gmail.js.map +1 -1
  115. package/dist/connectors/googleCalendar.d.ts +4 -1
  116. package/dist/connectors/googleCalendar.js +88 -25
  117. package/dist/connectors/googleCalendar.js.map +1 -1
  118. package/dist/connectors/googleDrive.d.ts +34 -0
  119. package/dist/connectors/googleDrive.js +321 -0
  120. package/dist/connectors/googleDrive.js.map +1 -0
  121. package/dist/connectors/htmlEscape.d.ts +5 -0
  122. package/dist/connectors/htmlEscape.js +13 -0
  123. package/dist/connectors/htmlEscape.js.map +1 -0
  124. package/dist/connectors/hubspot.d.ts +112 -0
  125. package/dist/connectors/hubspot.js +408 -0
  126. package/dist/connectors/hubspot.js.map +1 -0
  127. package/dist/connectors/intercom.d.ts +102 -0
  128. package/dist/connectors/intercom.js +402 -0
  129. package/dist/connectors/intercom.js.map +1 -0
  130. package/dist/connectors/jira.d.ts +98 -0
  131. package/dist/connectors/jira.js +396 -0
  132. package/dist/connectors/jira.js.map +1 -0
  133. package/dist/connectors/linear.js +30 -19
  134. package/dist/connectors/linear.js.map +1 -1
  135. package/dist/connectors/mcpOAuth.d.ts +3 -0
  136. package/dist/connectors/mcpOAuth.js +64 -10
  137. package/dist/connectors/mcpOAuth.js.map +1 -1
  138. package/dist/connectors/mockConnector.d.ts +28 -0
  139. package/dist/connectors/mockConnector.js +81 -0
  140. package/dist/connectors/mockConnector.js.map +1 -0
  141. package/dist/connectors/notion.d.ts +143 -0
  142. package/dist/connectors/notion.js +424 -0
  143. package/dist/connectors/notion.js.map +1 -0
  144. package/dist/connectors/oauthStateStore.d.ts +31 -0
  145. package/dist/connectors/oauthStateStore.js +52 -0
  146. package/dist/connectors/oauthStateStore.js.map +1 -0
  147. package/dist/connectors/pagerduty.d.ts +160 -0
  148. package/dist/connectors/pagerduty.js +464 -0
  149. package/dist/connectors/pagerduty.js.map +1 -0
  150. package/dist/connectors/sentry.js +5 -13
  151. package/dist/connectors/sentry.js.map +1 -1
  152. package/dist/connectors/slack.d.ts +16 -1
  153. package/dist/connectors/slack.js +155 -32
  154. package/dist/connectors/slack.js.map +1 -1
  155. package/dist/connectors/stripe.d.ts +116 -0
  156. package/dist/connectors/stripe.js +379 -0
  157. package/dist/connectors/stripe.js.map +1 -0
  158. package/dist/connectors/tokenStorage.d.ts +35 -0
  159. package/dist/connectors/tokenStorage.js +484 -0
  160. package/dist/connectors/tokenStorage.js.map +1 -0
  161. package/dist/connectors/zendesk.d.ts +104 -0
  162. package/dist/connectors/zendesk.js +442 -0
  163. package/dist/connectors/zendesk.js.map +1 -0
  164. package/dist/cors.d.ts +10 -0
  165. package/dist/cors.js +29 -0
  166. package/dist/cors.js.map +1 -0
  167. package/dist/decisionReplay.d.ts +72 -0
  168. package/dist/decisionReplay.js +92 -0
  169. package/dist/decisionReplay.js.map +1 -0
  170. package/dist/decisionTraceLog.d.ts +6 -0
  171. package/dist/decisionTraceLog.js +54 -2
  172. package/dist/decisionTraceLog.js.map +1 -1
  173. package/dist/drivers/claude/subprocess.d.ts +12 -2
  174. package/dist/drivers/claude/subprocess.js +79 -6
  175. package/dist/drivers/claude/subprocess.js.map +1 -1
  176. package/dist/drivers/gemini/api.d.ts +18 -0
  177. package/dist/drivers/gemini/api.js +29 -0
  178. package/dist/drivers/gemini/api.js.map +1 -0
  179. package/dist/drivers/gemini/index.d.ts +5 -1
  180. package/dist/drivers/gemini/index.js +39 -5
  181. package/dist/drivers/gemini/index.js.map +1 -1
  182. package/dist/drivers/index.d.ts +8 -1
  183. package/dist/drivers/index.js +10 -2
  184. package/dist/drivers/index.js.map +1 -1
  185. package/dist/drivers/local/index.d.ts +26 -0
  186. package/dist/drivers/local/index.js +41 -0
  187. package/dist/drivers/local/index.js.map +1 -0
  188. package/dist/featureFlags.d.ts +79 -0
  189. package/dist/featureFlags.js +208 -0
  190. package/dist/featureFlags.js.map +1 -0
  191. package/dist/fp/automationInterpreter.js +26 -21
  192. package/dist/fp/automationInterpreter.js.map +1 -1
  193. package/dist/fp/automationProgram.d.ts +1 -1
  194. package/dist/fp/automationProgram.js.map +1 -1
  195. package/dist/fp/automationState.js +4 -1
  196. package/dist/fp/automationState.js.map +1 -1
  197. package/dist/fp/policyParser.js +21 -1
  198. package/dist/fp/policyParser.js.map +1 -1
  199. package/dist/httpErrorResponse.d.ts +36 -0
  200. package/dist/httpErrorResponse.js +46 -0
  201. package/dist/httpErrorResponse.js.map +1 -0
  202. package/dist/inboxRoutes.d.ts +22 -0
  203. package/dist/inboxRoutes.js +193 -0
  204. package/dist/inboxRoutes.js.map +1 -0
  205. package/dist/index.d.ts +1 -1
  206. package/dist/index.js +1403 -203
  207. package/dist/index.js.map +1 -1
  208. package/dist/installGuard.d.ts +25 -0
  209. package/dist/installGuard.js +48 -0
  210. package/dist/installGuard.js.map +1 -0
  211. package/dist/mcpRoutes.d.ts +37 -0
  212. package/dist/mcpRoutes.js +76 -0
  213. package/dist/mcpRoutes.js.map +1 -0
  214. package/dist/oauth.d.ts +20 -1
  215. package/dist/oauth.js +214 -39
  216. package/dist/oauth.js.map +1 -1
  217. package/dist/oauthRoutes.d.ts +32 -0
  218. package/dist/oauthRoutes.js +119 -0
  219. package/dist/oauthRoutes.js.map +1 -0
  220. package/dist/orchestrator/orchestratorBridge.js +2 -2
  221. package/dist/orchestrator/orchestratorBridge.js.map +1 -1
  222. package/dist/patchworkConfig.d.ts +29 -0
  223. package/dist/patchworkConfig.js +100 -5
  224. package/dist/patchworkConfig.js.map +1 -1
  225. package/dist/pluginLoader.d.ts +28 -0
  226. package/dist/pluginLoader.js +77 -11
  227. package/dist/pluginLoader.js.map +1 -1
  228. package/dist/pluginWatcher.js +8 -3
  229. package/dist/pluginWatcher.js.map +1 -1
  230. package/dist/preToolUseHook.d.ts +12 -0
  231. package/dist/preToolUseHook.js +30 -1
  232. package/dist/preToolUseHook.js.map +1 -1
  233. package/dist/prompts.js +4 -0
  234. package/dist/prompts.js.map +1 -1
  235. package/dist/recipeOrchestration.d.ts +121 -0
  236. package/dist/recipeOrchestration.js +965 -0
  237. package/dist/recipeOrchestration.js.map +1 -0
  238. package/dist/recipeRoutes.d.ts +185 -0
  239. package/dist/recipeRoutes.js +1369 -0
  240. package/dist/recipeRoutes.js.map +1 -0
  241. package/dist/recipes/RecipeOrchestrator.d.ts +40 -0
  242. package/dist/recipes/RecipeOrchestrator.js +51 -0
  243. package/dist/recipes/RecipeOrchestrator.js.map +1 -0
  244. package/dist/recipes/agentExecutor.d.ts +38 -0
  245. package/dist/recipes/agentExecutor.js +50 -0
  246. package/dist/recipes/agentExecutor.js.map +1 -0
  247. package/dist/recipes/chainedRunner.d.ts +191 -0
  248. package/dist/recipes/chainedRunner.js +759 -0
  249. package/dist/recipes/chainedRunner.js.map +1 -0
  250. package/dist/recipes/compiler.js +3 -3
  251. package/dist/recipes/compiler.js.map +1 -1
  252. package/dist/recipes/dependencyGraph.d.ts +39 -0
  253. package/dist/recipes/dependencyGraph.js +199 -0
  254. package/dist/recipes/dependencyGraph.js.map +1 -0
  255. package/dist/recipes/disabledMarkers.d.ts +48 -0
  256. package/dist/recipes/disabledMarkers.js +52 -0
  257. package/dist/recipes/disabledMarkers.js.map +1 -0
  258. package/dist/recipes/installer.js +3 -3
  259. package/dist/recipes/installer.js.map +1 -1
  260. package/dist/recipes/legacyRecipeCompat.d.ts +10 -0
  261. package/dist/recipes/legacyRecipeCompat.js +131 -0
  262. package/dist/recipes/legacyRecipeCompat.js.map +1 -0
  263. package/dist/recipes/manifest.d.ts +47 -0
  264. package/dist/recipes/manifest.js +156 -0
  265. package/dist/recipes/manifest.js.map +1 -0
  266. package/dist/recipes/migrationWarnings.d.ts +12 -0
  267. package/dist/recipes/migrationWarnings.js +44 -0
  268. package/dist/recipes/migrationWarnings.js.map +1 -0
  269. package/dist/recipes/migrations/index.d.ts +24 -0
  270. package/dist/recipes/migrations/index.js +55 -0
  271. package/dist/recipes/migrations/index.js.map +1 -0
  272. package/dist/recipes/migrations/types.d.ts +28 -0
  273. package/dist/recipes/migrations/types.js +2 -0
  274. package/dist/recipes/migrations/types.js.map +1 -0
  275. package/dist/recipes/migrations/v1.d.ts +11 -0
  276. package/dist/recipes/migrations/v1.js +18 -0
  277. package/dist/recipes/migrations/v1.js.map +1 -0
  278. package/dist/recipes/names.d.ts +40 -0
  279. package/dist/recipes/names.js +66 -0
  280. package/dist/recipes/names.js.map +1 -0
  281. package/dist/recipes/nestedRecipeStep.d.ts +58 -0
  282. package/dist/recipes/nestedRecipeStep.js +95 -0
  283. package/dist/recipes/nestedRecipeStep.js.map +1 -0
  284. package/dist/recipes/outputRegistry.d.ts +28 -0
  285. package/dist/recipes/outputRegistry.js +52 -0
  286. package/dist/recipes/outputRegistry.js.map +1 -0
  287. package/dist/recipes/parser.js +4 -1
  288. package/dist/recipes/parser.js.map +1 -1
  289. package/dist/recipes/replayRun.d.ts +62 -0
  290. package/dist/recipes/replayRun.js +97 -0
  291. package/dist/recipes/replayRun.js.map +1 -0
  292. package/dist/recipes/resolveRecipePath.d.ts +69 -0
  293. package/dist/recipes/resolveRecipePath.js +202 -0
  294. package/dist/recipes/resolveRecipePath.js.map +1 -0
  295. package/dist/recipes/scheduler.d.ts +23 -7
  296. package/dist/recipes/scheduler.js +225 -45
  297. package/dist/recipes/scheduler.js.map +1 -1
  298. package/dist/recipes/schema.d.ts +17 -2
  299. package/dist/recipes/schemaGenerator.d.ts +28 -0
  300. package/dist/recipes/schemaGenerator.js +565 -0
  301. package/dist/recipes/schemaGenerator.js.map +1 -0
  302. package/dist/recipes/stepObservation.d.ts +44 -0
  303. package/dist/recipes/stepObservation.js +232 -0
  304. package/dist/recipes/stepObservation.js.map +1 -0
  305. package/dist/recipes/templateEngine.d.ts +62 -0
  306. package/dist/recipes/templateEngine.js +201 -0
  307. package/dist/recipes/templateEngine.js.map +1 -0
  308. package/dist/recipes/toolRegistry.d.ts +186 -0
  309. package/dist/recipes/toolRegistry.js +309 -0
  310. package/dist/recipes/toolRegistry.js.map +1 -0
  311. package/dist/recipes/tools/asana.d.ts +16 -0
  312. package/dist/recipes/tools/asana.js +524 -0
  313. package/dist/recipes/tools/asana.js.map +1 -0
  314. package/dist/recipes/tools/calendar.d.ts +6 -0
  315. package/dist/recipes/tools/calendar.js +61 -0
  316. package/dist/recipes/tools/calendar.js.map +1 -0
  317. package/dist/recipes/tools/confluence.d.ts +6 -0
  318. package/dist/recipes/tools/confluence.js +254 -0
  319. package/dist/recipes/tools/confluence.js.map +1 -0
  320. package/dist/recipes/tools/datadog.d.ts +6 -0
  321. package/dist/recipes/tools/datadog.js +239 -0
  322. package/dist/recipes/tools/datadog.js.map +1 -0
  323. package/dist/recipes/tools/diagnostics.d.ts +6 -0
  324. package/dist/recipes/tools/diagnostics.js +36 -0
  325. package/dist/recipes/tools/diagnostics.js.map +1 -0
  326. package/dist/recipes/tools/discord.d.ts +18 -0
  327. package/dist/recipes/tools/discord.js +254 -0
  328. package/dist/recipes/tools/discord.js.map +1 -0
  329. package/dist/recipes/tools/file.d.ts +12 -0
  330. package/dist/recipes/tools/file.js +174 -0
  331. package/dist/recipes/tools/file.js.map +1 -0
  332. package/dist/recipes/tools/git.d.ts +6 -0
  333. package/dist/recipes/tools/git.js +63 -0
  334. package/dist/recipes/tools/git.js.map +1 -0
  335. package/dist/recipes/tools/github.d.ts +6 -0
  336. package/dist/recipes/tools/github.js +116 -0
  337. package/dist/recipes/tools/github.js.map +1 -0
  338. package/dist/recipes/tools/gitlab.d.ts +11 -0
  339. package/dist/recipes/tools/gitlab.js +285 -0
  340. package/dist/recipes/tools/gitlab.js.map +1 -0
  341. package/dist/recipes/tools/gmail.d.ts +6 -0
  342. package/dist/recipes/tools/gmail.js +451 -0
  343. package/dist/recipes/tools/gmail.js.map +1 -0
  344. package/dist/recipes/tools/googleDrive.d.ts +1 -0
  345. package/dist/recipes/tools/googleDrive.js +55 -0
  346. package/dist/recipes/tools/googleDrive.js.map +1 -0
  347. package/dist/recipes/tools/hubspot.d.ts +6 -0
  348. package/dist/recipes/tools/hubspot.js +232 -0
  349. package/dist/recipes/tools/hubspot.js.map +1 -0
  350. package/dist/recipes/tools/index.d.ts +30 -0
  351. package/dist/recipes/tools/index.js +33 -0
  352. package/dist/recipes/tools/index.js.map +1 -0
  353. package/dist/recipes/tools/intercom.d.ts +6 -0
  354. package/dist/recipes/tools/intercom.js +226 -0
  355. package/dist/recipes/tools/intercom.js.map +1 -0
  356. package/dist/recipes/tools/jira.d.ts +14 -0
  357. package/dist/recipes/tools/jira.js +369 -0
  358. package/dist/recipes/tools/jira.js.map +1 -0
  359. package/dist/recipes/tools/linear.d.ts +7 -0
  360. package/dist/recipes/tools/linear.js +307 -0
  361. package/dist/recipes/tools/linear.js.map +1 -0
  362. package/dist/recipes/tools/meetingNotes.d.ts +21 -0
  363. package/dist/recipes/tools/meetingNotes.js +701 -0
  364. package/dist/recipes/tools/meetingNotes.js.map +1 -0
  365. package/dist/recipes/tools/notion.d.ts +6 -0
  366. package/dist/recipes/tools/notion.js +278 -0
  367. package/dist/recipes/tools/notion.js.map +1 -0
  368. package/dist/recipes/tools/pagerduty.d.ts +15 -0
  369. package/dist/recipes/tools/pagerduty.js +451 -0
  370. package/dist/recipes/tools/pagerduty.js.map +1 -0
  371. package/dist/recipes/tools/sentry.d.ts +12 -0
  372. package/dist/recipes/tools/sentry.js +73 -0
  373. package/dist/recipes/tools/sentry.js.map +1 -0
  374. package/dist/recipes/tools/slack.d.ts +6 -0
  375. package/dist/recipes/tools/slack.js +82 -0
  376. package/dist/recipes/tools/slack.js.map +1 -0
  377. package/dist/recipes/tools/stripe.d.ts +6 -0
  378. package/dist/recipes/tools/stripe.js +265 -0
  379. package/dist/recipes/tools/stripe.js.map +1 -0
  380. package/dist/recipes/tools/zendesk.d.ts +6 -0
  381. package/dist/recipes/tools/zendesk.js +245 -0
  382. package/dist/recipes/tools/zendesk.js.map +1 -0
  383. package/dist/recipes/validation.d.ts +13 -0
  384. package/dist/recipes/validation.js +617 -0
  385. package/dist/recipes/validation.js.map +1 -0
  386. package/dist/recipes/yamlRunner.d.ts +130 -2
  387. package/dist/recipes/yamlRunner.js +1009 -402
  388. package/dist/recipes/yamlRunner.js.map +1 -1
  389. package/dist/recipesHttp.d.ts +151 -6
  390. package/dist/recipesHttp.js +999 -29
  391. package/dist/recipesHttp.js.map +1 -1
  392. package/dist/riskTier.js +7 -1
  393. package/dist/riskTier.js.map +1 -1
  394. package/dist/runLog.d.ts +100 -1
  395. package/dist/runLog.js +258 -5
  396. package/dist/runLog.js.map +1 -1
  397. package/dist/schemas/dry-run-plan.v1.json +139 -0
  398. package/dist/schemas/recipe.v1.json +684 -0
  399. package/dist/server.d.ts +127 -8
  400. package/dist/server.js +740 -933
  401. package/dist/server.js.map +1 -1
  402. package/dist/ssrfGuard.d.ts +54 -0
  403. package/dist/ssrfGuard.js +122 -0
  404. package/dist/ssrfGuard.js.map +1 -0
  405. package/dist/streamableHttp.d.ts +39 -1
  406. package/dist/streamableHttp.js +128 -17
  407. package/dist/streamableHttp.js.map +1 -1
  408. package/dist/tokenUsageTracker.d.ts +33 -0
  409. package/dist/tokenUsageTracker.js +146 -0
  410. package/dist/tokenUsageTracker.js.map +1 -0
  411. package/dist/tools/activityLog.d.ts +2 -0
  412. package/dist/tools/addLinearComment.d.ts +1 -0
  413. package/dist/tools/addLinearComment.js +4 -2
  414. package/dist/tools/addLinearComment.js.map +1 -1
  415. package/dist/tools/batchLsp.d.ts +3 -0
  416. package/dist/tools/bridgeDoctor.d.ts +1 -0
  417. package/dist/tools/bridgeDoctor.js +2 -2
  418. package/dist/tools/bridgeDoctor.js.map +1 -1
  419. package/dist/tools/bridgeStatus.d.ts +1 -0
  420. package/dist/tools/cancelClaudeTask.d.ts +2 -0
  421. package/dist/tools/cancelClaudeTask.js +1 -0
  422. package/dist/tools/cancelClaudeTask.js.map +1 -1
  423. package/dist/tools/checkDocumentDirty.d.ts +1 -0
  424. package/dist/tools/clipboard.d.ts +2 -0
  425. package/dist/tools/closeTabs.d.ts +2 -0
  426. package/dist/tools/codeLens.d.ts +1 -0
  427. package/dist/tools/contextBundle.d.ts +1 -0
  428. package/dist/tools/createIssueFromAIComment.d.ts +1 -0
  429. package/dist/tools/createLinearIssue.d.ts +1 -0
  430. package/dist/tools/ctxGetTaskContext.d.ts +1 -0
  431. package/dist/tools/ctxQueryTraces.d.ts +1 -0
  432. package/dist/tools/ctxSaveTrace.d.ts +1 -0
  433. package/dist/tools/debug.d.ts +4 -0
  434. package/dist/tools/decorations.d.ts +2 -0
  435. package/dist/tools/documentLinks.d.ts +1 -0
  436. package/dist/tools/editText.d.ts +1 -0
  437. package/dist/tools/enrichCommit.d.ts +1 -0
  438. package/dist/tools/enrichStackTrace.d.ts +1 -0
  439. package/dist/tools/explainDiagnostic.d.ts +1 -0
  440. package/dist/tools/explainSymbol.d.ts +1 -0
  441. package/dist/tools/fetchCalendarEvents.d.ts +1 -0
  442. package/dist/tools/fetchGithubIssue.d.ts +1 -0
  443. package/dist/tools/fetchGithubPR.d.ts +1 -0
  444. package/dist/tools/fetchLinearIssue.d.ts +1 -0
  445. package/dist/tools/fetchSentryIssue.d.ts +1 -0
  446. package/dist/tools/fetchSlackProfile.d.ts +1 -0
  447. package/dist/tools/fetchSlackProfile.js +4 -1
  448. package/dist/tools/fetchSlackProfile.js.map +1 -1
  449. package/dist/tools/fileOperations.d.ts +3 -0
  450. package/dist/tools/fileWatcher.d.ts +2 -0
  451. package/dist/tools/findFiles.d.ts +1 -0
  452. package/dist/tools/findRelatedTests.d.ts +1 -0
  453. package/dist/tools/fixAllLintErrors.d.ts +1 -0
  454. package/dist/tools/foldingRanges.d.ts +1 -0
  455. package/dist/tools/formatDocument.d.ts +1 -0
  456. package/dist/tools/generateTests.d.ts +1 -0
  457. package/dist/tools/getAIComments.d.ts +1 -0
  458. package/dist/tools/getAnalyticsReport.d.ts +1 -0
  459. package/dist/tools/getArchitectureContext.d.ts +1 -0
  460. package/dist/tools/getBufferContent.d.ts +1 -0
  461. package/dist/tools/getChangeImpact.d.ts +1 -0
  462. package/dist/tools/getClaudeTaskStatus.d.ts +2 -0
  463. package/dist/tools/getClaudeTaskStatus.js +1 -0
  464. package/dist/tools/getClaudeTaskStatus.js.map +1 -1
  465. package/dist/tools/getCodeCoverage.d.ts +1 -0
  466. package/dist/tools/getCommitsForIssue.d.ts +1 -0
  467. package/dist/tools/getConnectorStatus.d.ts +1 -0
  468. package/dist/tools/getCurrentSelection.d.ts +2 -0
  469. package/dist/tools/getDebugState.d.ts +1 -0
  470. package/dist/tools/getDependencyTree.d.ts +1 -0
  471. package/dist/tools/getDiagnostics.d.ts +1 -0
  472. package/dist/tools/getDiffFromHandoff.d.ts +1 -0
  473. package/dist/tools/getDocumentSymbols.d.ts +25 -0
  474. package/dist/tools/getDocumentSymbols.js +74 -8
  475. package/dist/tools/getDocumentSymbols.js.map +1 -1
  476. package/dist/tools/getFileTree.d.ts +1 -0
  477. package/dist/tools/getGitDiff.d.ts +1 -0
  478. package/dist/tools/getGitHotspots.d.ts +1 -0
  479. package/dist/tools/getGitLog.d.ts +1 -0
  480. package/dist/tools/getGitStatus.d.ts +1 -0
  481. package/dist/tools/getImportTree.d.ts +1 -0
  482. package/dist/tools/getImportedSignatures.d.ts +1 -0
  483. package/dist/tools/getOpenEditors.d.ts +1 -0
  484. package/dist/tools/getPRTemplate.d.ts +1 -0
  485. package/dist/tools/getProjectContext.d.ts +1 -0
  486. package/dist/tools/getProjectInfo.d.ts +1 -0
  487. package/dist/tools/getSecurityAdvisories.d.ts +1 -0
  488. package/dist/tools/getSecurityAdvisories.js +10 -1
  489. package/dist/tools/getSecurityAdvisories.js.map +1 -1
  490. package/dist/tools/getSessionUsage.d.ts +4 -0
  491. package/dist/tools/getSessionUsage.js +3 -0
  492. package/dist/tools/getSessionUsage.js.map +1 -1
  493. package/dist/tools/getSymbolHistory.d.ts +1 -0
  494. package/dist/tools/getToolCapabilities.d.ts +1 -0
  495. package/dist/tools/getTypeSignature.d.ts +1 -0
  496. package/dist/tools/getWorkspaceFolders.d.ts +1 -0
  497. package/dist/tools/getWorkspaceSettings.d.ts +1 -0
  498. package/dist/tools/gitHistory.d.ts +2 -0
  499. package/dist/tools/gitWrite.d.ts +11 -0
  500. package/dist/tools/github/actions.d.ts +2 -0
  501. package/dist/tools/github/actions.js +4 -2
  502. package/dist/tools/github/actions.js.map +1 -1
  503. package/dist/tools/github/composite.d.ts +342 -0
  504. package/dist/tools/github/composite.js +343 -0
  505. package/dist/tools/github/composite.js.map +1 -0
  506. package/dist/tools/github/index.d.ts +1 -0
  507. package/dist/tools/github/index.js +1 -0
  508. package/dist/tools/github/index.js.map +1 -1
  509. package/dist/tools/github/issues.d.ts +4 -0
  510. package/dist/tools/github/issues.js +8 -4
  511. package/dist/tools/github/issues.js.map +1 -1
  512. package/dist/tools/github/pr.d.ts +7 -0
  513. package/dist/tools/github/pr.js +50 -12
  514. package/dist/tools/github/pr.js.map +1 -1
  515. package/dist/tools/handoffNote.d.ts +4 -0
  516. package/dist/tools/handoffNote.js +2 -0
  517. package/dist/tools/handoffNote.js.map +1 -1
  518. package/dist/tools/hoverAtCursor.d.ts +1 -0
  519. package/dist/tools/httpClient.d.ts +2 -0
  520. package/dist/tools/index.d.ts +8 -0
  521. package/dist/tools/index.js +47 -8
  522. package/dist/tools/index.js.map +1 -1
  523. package/dist/tools/inlayHints.d.ts +1 -0
  524. package/dist/tools/launchQuickTask.d.ts +2 -0
  525. package/dist/tools/launchQuickTask.js +1 -0
  526. package/dist/tools/launchQuickTask.js.map +1 -1
  527. package/dist/tools/listClaudeTasks.d.ts +2 -0
  528. package/dist/tools/listClaudeTasks.js +1 -0
  529. package/dist/tools/listClaudeTasks.js.map +1 -1
  530. package/dist/tools/listTerminals.d.ts +1 -0
  531. package/dist/tools/lsp.d.ts +14 -0
  532. package/dist/tools/navigateToSymbolByName.d.ts +1 -0
  533. package/dist/tools/openDiff.d.ts +1 -0
  534. package/dist/tools/openFile.d.ts +1 -0
  535. package/dist/tools/openInBrowser.d.ts +1 -0
  536. package/dist/tools/organizeImports.d.ts +1 -0
  537. package/dist/tools/performanceReport.d.ts +1 -0
  538. package/dist/tools/planPersistence.d.ts +5 -0
  539. package/dist/tools/previewEdit.d.ts +1 -0
  540. package/dist/tools/refactorAnalyze.d.ts +1 -0
  541. package/dist/tools/refactorPreview.d.ts +2 -0
  542. package/dist/tools/refactorPreview.js +1 -0
  543. package/dist/tools/refactorPreview.js.map +1 -1
  544. package/dist/tools/replaceBlock.d.ts +1 -0
  545. package/dist/tools/resumeClaudeTask.d.ts +2 -0
  546. package/dist/tools/resumeClaudeTask.js +1 -0
  547. package/dist/tools/resumeClaudeTask.js.map +1 -1
  548. package/dist/tools/runClaudeTask.d.ts +2 -0
  549. package/dist/tools/runClaudeTask.js +1 -0
  550. package/dist/tools/runClaudeTask.js.map +1 -1
  551. package/dist/tools/runCommand.d.ts +1 -0
  552. package/dist/tools/runCommand.js +5 -0
  553. package/dist/tools/runCommand.js.map +1 -1
  554. package/dist/tools/runTests.d.ts +1 -0
  555. package/dist/tools/saveDocument.d.ts +1 -0
  556. package/dist/tools/screenshotAndAnnotate.d.ts +1 -0
  557. package/dist/tools/searchAndReplace.d.ts +1 -0
  558. package/dist/tools/searchTools.d.ts +1 -0
  559. package/dist/tools/searchTools.js +1 -1
  560. package/dist/tools/searchTools.js.map +1 -1
  561. package/dist/tools/searchWorkspace.d.ts +1 -0
  562. package/dist/tools/selectionRanges.d.ts +1 -0
  563. package/dist/tools/semanticTokens.d.ts +1 -0
  564. package/dist/tools/setActiveWorkspaceFolder.d.ts +1 -0
  565. package/dist/tools/signatureHelp.d.ts +1 -0
  566. package/dist/tools/slackListChannels.d.ts +1 -0
  567. package/dist/tools/slackListChannels.js.map +1 -1
  568. package/dist/tools/slackPostMessage.d.ts +1 -0
  569. package/dist/tools/slackPostMessage.js +11 -6
  570. package/dist/tools/slackPostMessage.js.map +1 -1
  571. package/dist/tools/terminal.d.ts +6 -0
  572. package/dist/tools/terminal.js +4 -0
  573. package/dist/tools/terminal.js.map +1 -1
  574. package/dist/tools/testTraceToSource.d.ts +1 -0
  575. package/dist/tools/testTraceToSource.js +2 -2
  576. package/dist/tools/testTraceToSource.js.map +1 -1
  577. package/dist/tools/transaction.d.ts +23 -0
  578. package/dist/tools/transaction.js +29 -0
  579. package/dist/tools/transaction.js.map +1 -1
  580. package/dist/tools/typeHierarchy.d.ts +1 -0
  581. package/dist/tools/updateLinearIssue.d.ts +1 -0
  582. package/dist/tools/updateLinearIssue.js +20 -6
  583. package/dist/tools/updateLinearIssue.js.map +1 -1
  584. package/dist/tools/utils.d.ts +6 -0
  585. package/dist/tools/utils.js +59 -0
  586. package/dist/tools/utils.js.map +1 -1
  587. package/dist/tools/vscodeCommands.d.ts +2 -0
  588. package/dist/tools/vscodeTasks.d.ts +2 -0
  589. package/dist/tools/workspaceSettings.d.ts +1 -0
  590. package/dist/traceEncryption.d.ts +46 -0
  591. package/dist/traceEncryption.js +124 -0
  592. package/dist/traceEncryption.js.map +1 -0
  593. package/dist/transport.d.ts +46 -1
  594. package/dist/transport.js +173 -19
  595. package/dist/transport.js.map +1 -1
  596. package/package.json +30 -8
  597. package/scripts/mcp-stdio-shim.cjs +19 -3
  598. package/scripts/start-all.sh +34 -3
  599. package/templates/automation-policies/recipe-authoring.json +25 -0
  600. package/templates/automation-policy.example.json +6 -0
  601. package/templates/co.patchwork-os.bridge.plist +34 -0
  602. package/templates/policies/README.md +72 -0
  603. package/templates/policies/conservative.json +14 -0
  604. package/templates/policies/developer.json +14 -0
  605. package/templates/policies/headless-ci.json +24 -0
  606. package/templates/policies/personal-assistant.json +15 -0
  607. package/templates/policies/regulated-industry.json +18 -0
  608. package/templates/recipes/approval-queue-ui-test.yaml +205 -0
  609. package/templates/recipes/lint-on-save.yaml +1 -2
  610. package/templates/recipes/morning-brief-slack.yaml +57 -0
  611. package/templates/recipes/morning-brief.yaml +2 -2
  612. package/templates/recipes/project-health-check.yaml +50 -0
  613. package/templates/recipes/webhook/README.md +70 -0
  614. package/templates/recipes/webhook/capture-thought.yaml +26 -0
  615. package/templates/recipes/webhook/customer-escalation.yaml +49 -0
  616. package/templates/recipes/webhook/incident-intake.yaml +46 -0
  617. package/templates/recipes/webhook/meeting-prep.yaml +48 -0
  618. package/templates/recipes/webhook/morning-brief.yaml +57 -0
package/dist/server.js CHANGED
@@ -1,14 +1,19 @@
1
1
  import { EventEmitter } from "node:events";
2
2
  import http from "node:http";
3
- import os from "node:os";
4
- import path from "node:path";
5
3
  import { WebSocket, WebSocketServer as WsServer } from "ws";
6
- import { routeApprovalRequest } from "./approvalHttp.js";
4
+ import { handleApprovalsStream, routeApprovalRequest } from "./approvalHttp.js";
7
5
  import { getApprovalQueue } from "./approvalQueue.js";
6
+ import { saveBridgeConfigDriver } from "./config.js";
7
+ import { tryHandleConnectorRoute, tryHandlePublicConnectorRoute, } from "./connectorRoutes.js";
8
8
  import { timingSafeStringEqual } from "./crypto.js";
9
9
  import { renderDashboardHtml } from "./dashboard.js";
10
- import { loadConfig as loadPatchworkConfig, defaultConfigPath as patchworkConfigPath, saveConfig as savePatchworkConfig, } from "./patchworkConfig.js";
11
- import { BRIDGE_PROTOCOL_VERSION, PACKAGE_LICENSE, PACKAGE_VERSION, } from "./version.js";
10
+ import { respond500 } from "./httpErrorResponse.js";
11
+ import { tryHandleInboxRoute } from "./inboxRoutes.js";
12
+ import { tryHandleMcpRoute } from "./mcpRoutes.js";
13
+ import { tryHandleOAuthRoute } from "./oauthRoutes.js";
14
+ import { loadConfig as loadPatchworkConfig, defaultConfigPath as patchworkConfigPath, saveApiKeyToSecureStore, saveConfig as savePatchworkConfig, } from "./patchworkConfig.js";
15
+ import { readBodyWithCap, readJsonBody, respond413, tryHandleRecipeRoute, } from "./recipeRoutes.js";
16
+ import { PACKAGE_VERSION } from "./version.js";
12
17
  const LOOPBACK_HOSTS = new Set(["localhost", "127.0.0.1", "[::1]"]);
13
18
  const SESSION_ID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
14
19
  function enableTcpKeepalive(ws) {
@@ -18,30 +23,10 @@ function enableTcpKeepalive(ws) {
18
23
  rawSocket.setKeepAlive(true, 60_000); // 60s TCP keepalive as defense-in-depth
19
24
  }
20
25
  }
21
- /**
22
- * Return the CORS origin to reflect, or null if the origin is untrusted.
23
- * Loopback origins are always allowed. Additional origins can be passed via
24
- * --cors-origin (e.g. https://claude.ai for remote deployments).
25
- */
26
- export function corsOrigin(requestOrigin, extraOrigins = []) {
27
- if (!requestOrigin)
28
- return null;
29
- if (extraOrigins.includes(requestOrigin))
30
- return requestOrigin;
31
- try {
32
- const { hostname, protocol } = new URL(requestOrigin);
33
- if (protocol === "http:" &&
34
- (hostname === "localhost" ||
35
- hostname === "127.0.0.1" ||
36
- hostname === "[::1]")) {
37
- return requestOrigin;
38
- }
39
- }
40
- catch {
41
- // malformed origin — deny
42
- }
43
- return null;
44
- }
26
+ import { corsOrigin } from "./cors.js";
27
+ // Re-exported for streamableHttp.ts and any external callers; new code
28
+ // should import directly from "./cors.js".
29
+ export { corsOrigin };
45
30
  // Re-export canonical constant-time comparison for use in this module.
46
31
  // Implementation lives in src/crypto.ts — see there for security notes.
47
32
  const timingSafeTokenCompare = timingSafeStringEqual;
@@ -74,6 +59,8 @@ export class Server extends EventEmitter {
74
59
  oauthServer = null;
75
60
  oauthIssuerUrl = null;
76
61
  sseSubscriberCount = 0;
62
+ /** Cache for CC permission rules (30s TTL) to avoid filesystem walks on each dashboard poll */
63
+ _explainRulesCache = null;
77
64
  static MAX_SSE_SUBSCRIBERS = 20;
78
65
  /** Set by bridge to provide health data */
79
66
  healthDataFn = null;
@@ -87,24 +74,89 @@ export class Server extends EventEmitter {
87
74
  readyFn = null;
88
75
  /** Set by bridge to provide task list data (sanitized — no raw prompts) */
89
76
  tasksFn = null;
77
+ /** Set by bridge to cancel a running/pending task by id. Returns true if found. */
78
+ cancelTaskFn = null;
79
+ /** Patchwork: set by bridge to set the trust level for a recipe by name. */
80
+ setRecipeTrustFn = null;
81
+ /** Patchwork: set by bridge to generate a recipe YAML draft from a natural-language prompt. */
82
+ generateRecipeFn = null;
90
83
  /** Patchwork: set by bridge to list installed recipes for the dashboard. */
91
84
  recipesFn = null;
85
+ /** Patchwork: set by bridge to load raw recipe source content by name. */
86
+ loadRecipeContentFn = null;
87
+ /** Patchwork: set by bridge to save raw recipe source content by name. */
88
+ saveRecipeContentFn = null;
89
+ /** Patchwork: set by bridge to delete a recipe by name. */
90
+ deleteRecipeContentFn = null;
91
+ /** Patchwork: set by bridge to archive a recipe (move to .archive/). */
92
+ archiveRecipeFn = null;
93
+ /** Patchwork: set by bridge to promote a variant recipe to the canonical name. */
94
+ promoteRecipeVariantFn = null;
95
+ /** Patchwork: set by bridge to duplicate a recipe as a variant. */
96
+ duplicateRecipeFn = null;
97
+ /** Patchwork: set by bridge to lint raw recipe content without saving. */
98
+ lintRecipeContentFn = null;
92
99
  /** Patchwork: set by bridge to save a new recipe draft to disk. */
93
100
  saveRecipeFn = null;
94
101
  /** Patchwork: set by bridge to query the recipe run audit log. */
95
102
  runsFn = null;
103
+ /** Patchwork: set by bridge to fetch a single run by seq for the detail page. */
104
+ runDetailFn = null;
105
+ /** Patchwork: set by bridge to generate a dry-run plan for a recipe by name. */
106
+ runPlanFn = null;
107
+ /** Patchwork (VD-4): mocked replay of an existing run. Returns the new
108
+ * run's seq plus any unmocked steps the caller may want to surface. */
109
+ runReplayFn = null;
96
110
  /** Patchwork: set by bridge to launch a named recipe via the orchestrator. */
97
111
  runRecipeFn = null;
98
112
  /** Patchwork: admin-controlled managed settings path (highest rule precedence). */
99
113
  managedSettingsPath = undefined;
114
+ /** Effective bridge config path to update when dashboard saves driver changes. */
115
+ bridgeConfigPath = undefined;
100
116
  /** Patchwork: live approval gate level — mutated by POST /settings, read by bridge per-session setup. */
101
117
  approvalGate = "off";
102
118
  /** Patchwork: outbound webhook URL for approval notifications (from dashboard.webhookUrl in config). */
103
119
  approvalWebhookUrl = undefined;
120
+ /** Patchwork: push relay service URL — when set, per-callId approval tokens are generated. */
121
+ pushServiceUrl = undefined;
122
+ /** Patchwork: bearer token for the push relay service. */
123
+ pushServiceToken = undefined;
124
+ /** Patchwork: public base URL of this bridge, embedded in push payloads as callback base. */
125
+ pushServiceBaseUrl = undefined;
104
126
  /** Patchwork: approval decision audit callback wired to activityLog.recordEvent. */
105
127
  onApprovalDecision = undefined;
128
+ /**
129
+ * Patchwork: activity log handle, used by approvalHttp to compute
130
+ * passive risk personalization signals (`src/approvalSignals.ts`).
131
+ * When unset, personalSignals are simply omitted from queue entries.
132
+ */
133
+ activityLog = undefined;
134
+ /**
135
+ * Patchwork: recipe-run log handle, used by approvalHttp for the
136
+ * "recipe-step trust" heuristic (h6 in src/approvalSignals.ts). When
137
+ * unset, h6 is silently skipped; the other personalSignals heuristics
138
+ * still compute as long as `activityLog` is wired.
139
+ */
140
+ recipeRunLog = undefined;
141
+ /**
142
+ * Patchwork: opt-in switch for personalSignals heuristic 10
143
+ * (time-of-day anomaly). Off by default — see config.ts. Threaded into
144
+ * routeApprovalRequest deps so the personalSignals computation honors
145
+ * the user's preference.
146
+ */
147
+ enableTimeOfDayAnomaly = false;
106
148
  /** Patchwork: set by bridge to match + fire webhook-triggered recipes. */
107
149
  webhookFn = null;
150
+ /**
151
+ * Patchwork: ring buffer of recent webhook payloads, keyed by path
152
+ * (e.g. "/incident-war-room"). The last MAX_WEBHOOK_PAYLOADS entries are
153
+ * retained per path so the dashboard can show what the recipe most
154
+ * recently received — answers "did the trigger fire? what did it send?"
155
+ * without forcing the user to dig through bridge logs. In-memory only;
156
+ * cleared on restart.
157
+ */
158
+ webhookPayloads = new Map();
159
+ static MAX_WEBHOOK_PAYLOADS = 5;
108
160
  /** Set by bridge to handle MCP Streamable HTTP sessions (POST/GET/DELETE /mcp) */
109
161
  httpMcpHandler = null;
110
162
  /** Set by bridge to subscribe a caller to real-time activity events. Returns unsubscribe fn. */
@@ -127,6 +179,7 @@ export class Server extends EventEmitter {
127
179
  sessionDetailFn = null;
128
180
  /** Set by bridge to handle POST /launch-quick-task — invokes launchQuickTask tool in-process. */
129
181
  launchQuickTaskFn = null;
182
+ setRecipeEnabledFn = null;
130
183
  /**
131
184
  * Attach an OAuth 2.0 Authorization Server.
132
185
  * When set, the bridge exposes:
@@ -181,141 +234,37 @@ export class Server extends EventEmitter {
181
234
  res.setHeader("Access-Control-Allow-Headers", "Content-Type, Authorization, Mcp-Session-Id");
182
235
  res.setHeader("Access-Control-Expose-Headers", "Mcp-Session-Id");
183
236
  }
184
- const parsedUrl = new URL(req.url ?? "/", "http://localhost");
185
- // ── OAuth 2.0 endpoints (unauthenticated handled before bearer check) ──
186
- // RFC 8414 discovery document
187
- if (parsedUrl.pathname === "/.well-known/oauth-authorization-server" &&
188
- req.method === "GET") {
189
- if (this.oauthServer) {
190
- this.oauthServer.handleDiscovery(res);
191
- }
192
- else {
193
- res.writeHead(404, { "Content-Type": "text/plain" });
194
- res.end("OAuth not configured");
195
- }
196
- return;
197
- }
198
- // RFC 9396 Protected Resource Metadata — Claude.ai probes this to discover
199
- // which authorization server protects this resource. Both the bare and
200
- // resource-path variants are handled.
201
- if (req.method === "GET" &&
202
- (parsedUrl.pathname === "/.well-known/oauth-protected-resource" ||
203
- parsedUrl.pathname.startsWith("/.well-known/oauth-protected-resource/"))) {
204
- if (this.oauthServer && this.oauthIssuerUrl) {
205
- res.writeHead(200, {
206
- "Content-Type": "application/json",
207
- "Cache-Control": "no-store",
208
- });
209
- res.end(JSON.stringify({
210
- resource: this.oauthIssuerUrl,
211
- authorization_servers: [this.oauthIssuerUrl],
212
- }));
213
- }
214
- else {
215
- res.writeHead(404, { "Content-Type": "text/plain" });
216
- res.end("OAuth not configured");
217
- }
218
- return;
219
- }
220
- // Authorization endpoint
221
- if (parsedUrl.pathname === "/oauth/authorize" &&
222
- (req.method === "GET" || req.method === "POST")) {
223
- if (this.oauthServer) {
224
- this.oauthServer.handleAuthorize(req, res);
225
- }
226
- else {
227
- res.writeHead(404, { "Content-Type": "text/plain" });
228
- res.end("OAuth not configured");
229
- }
230
- return;
231
- }
232
- // Dynamic Client Registration endpoint (RFC 7591)
233
- if (parsedUrl.pathname === "/oauth/register") {
234
- if (this.oauthServer) {
235
- this.oauthServer.handleRegister(req, res).catch((err) => {
236
- if (!res.headersSent) {
237
- res.writeHead(500, { "Content-Type": "application/json" });
238
- res.end(JSON.stringify({ error: String(err) }));
239
- }
240
- });
241
- }
242
- else {
243
- res.writeHead(404, { "Content-Type": "text/plain" });
244
- res.end("OAuth not configured");
245
- }
246
- return;
247
- }
248
- // Token endpoint
249
- if (parsedUrl.pathname === "/oauth/token" && req.method === "POST") {
250
- if (this.oauthServer) {
251
- this.oauthServer.handleToken(req, res).catch((err) => {
252
- if (!res.headersSent) {
253
- res.writeHead(500, { "Content-Type": "application/json" });
254
- res.end(JSON.stringify({ error: String(err) }));
255
- }
256
- });
257
- }
258
- else {
259
- res.writeHead(404, { "Content-Type": "text/plain" });
260
- res.end("OAuth not configured");
261
- }
262
- return;
263
- }
264
- // Revocation endpoint (RFC 7009)
265
- if (parsedUrl.pathname === "/oauth/revoke" && req.method === "POST") {
266
- if (this.oauthServer) {
267
- this.oauthServer.handleRevoke(req, res).catch(() => {
268
- // RFC 7009: always 200
269
- if (!res.headersSent) {
270
- res.writeHead(200, { "Content-Type": "application/json" });
271
- res.end("{}");
272
- }
273
- });
274
- }
275
- else {
276
- res.writeHead(200, { "Content-Type": "application/json" });
277
- res.end("{}");
278
- }
237
+ // DNS rebinding defense: validate Host header on every HTTP request,
238
+ // mirroring the WS upgrade handler below. Without this, attacker DNS
239
+ // can rebind a public hostname to 127.0.0.1 in the victim's browser
240
+ // and reach `/dashboard`, `/health`, `/metrics`, `/mcp`, OAuth
241
+ // endpoints, etc. with arbitrary Host headers. CORS gates browser
242
+ // *reads* of responses but does NOT gate top-level navigations or
243
+ // simple side-effect-bearing POSTs (e.g. `/oauth/authorize`).
244
+ const rawHost = req.headers.host ?? "";
245
+ const host = rawHost.startsWith("[")
246
+ ? rawHost.slice(0, rawHost.indexOf("]") + 1)
247
+ : rawHost.replace(/:\d+$/, "");
248
+ if (!host || !this.allowedHosts.has(host)) {
249
+ this.logger.warn(`Rejected HTTP request with invalid Host header: ${rawHost}`);
250
+ res.writeHead(403, { "Content-Type": "text/plain" });
251
+ res.end("Invalid Host header");
279
252
  return;
280
253
  }
281
- // ── MCP server-card (public) ──────────────────────────────────────────
282
- if (req.url === "/.well-known/mcp/server-card.json" ||
283
- req.url === "/.well-known/mcp") {
284
- const card = {
285
- name: "claude-ide-bridge",
286
- version: BRIDGE_PROTOCOL_VERSION,
287
- description: "MCP bridge providing full IDE integration for Claude Code — LSP, diagnostics, file operations, terminal, debug adapters, and AI task orchestration",
288
- homepage: "https://github.com/Oolab-labs/claude-ide-bridge",
289
- transport: ["websocket", "stdio", "streamable-http"],
290
- capabilities: {
291
- tools: true,
292
- resources: true,
293
- prompts: true,
294
- elicitation: true,
295
- },
296
- author: "Oolab Labs",
297
- license: PACKAGE_LICENSE,
298
- repository: "https://github.com/Oolab-labs/claude-ide-bridge",
299
- };
300
- res.writeHead(200, {
301
- "Content-Type": "application/json",
302
- "Access-Control-Allow-Origin": "*",
303
- });
304
- res.end(JSON.stringify(card, null, 2));
254
+ const parsedUrl = new URL(req.url ?? "/", "http://localhost");
255
+ // ── OAuth 2.0 endpoints (extracted to src/oauthRoutes.ts) ────────────
256
+ // Unauthenticated — must run BEFORE the bearer-auth gate.
257
+ if (tryHandleOAuthRoute(req, res, parsedUrl, {
258
+ oauthServer: this.oauthServer,
259
+ oauthIssuerUrl: this.oauthIssuerUrl,
260
+ })) {
305
261
  return;
306
262
  }
307
- // CORS preflight for /mcp browsers (and Claude Desktop's web renderer) send
308
- // OPTIONS before POST. Respond without requiring auth so the preflight succeeds.
309
- if (req.method === "OPTIONS" && parsedUrl.pathname === "/mcp") {
310
- const origin = corsOrigin(req.headers.origin, this.extraCorsOrigins);
311
- if (origin) {
312
- res.setHeader("Access-Control-Allow-Origin", origin);
313
- res.setHeader("Access-Control-Allow-Methods", "GET, POST, DELETE, OPTIONS");
314
- res.setHeader("Access-Control-Allow-Headers", "Content-Type, Authorization, Mcp-Session-Id");
315
- res.setHeader("Access-Control-Expose-Headers", "Mcp-Session-Id");
316
- }
317
- res.writeHead(204);
318
- res.end();
263
+ // ── MCP server-card + CORS preflight (extracted to src/mcpRoutes.ts)
264
+ // Unauthenticated must run BEFORE the bearer-auth gate.
265
+ if (tryHandleMcpRoute(req, res, parsedUrl, {
266
+ extraCorsOrigins: this.extraCorsOrigins,
267
+ })) {
319
268
  return;
320
269
  }
321
270
  // Unauthenticated liveness probe — safe to expose; contains no sensitive data.
@@ -355,78 +304,54 @@ export class Server extends EventEmitter {
355
304
  res.end(JSON.stringify({ ok: true, v: PACKAGE_VERSION }));
356
305
  return;
357
306
  }
358
- // ── Connector OAuth callbacks (unauthenticated browser redirect from vendor) ──
359
- if (parsedUrl.pathname === "/connections/github/callback" &&
360
- req.method === "GET") {
361
- void (async () => {
362
- const { handleGithubCallback } = await import("./connectors/github.js");
363
- const code = parsedUrl.searchParams.get("code");
364
- const state = parsedUrl.searchParams.get("state");
365
- const error = parsedUrl.searchParams.get("error");
366
- const result = await handleGithubCallback(code, state, error);
367
- res.writeHead(result.status, {
368
- "Content-Type": result.contentType ?? "application/json",
369
- });
370
- res.end(result.body);
371
- })();
372
- return;
373
- }
374
- if (parsedUrl.pathname === "/connections/linear/callback" &&
375
- req.method === "GET") {
376
- void (async () => {
377
- const { handleLinearCallback } = await import("./connectors/linear.js");
378
- const code = parsedUrl.searchParams.get("code");
379
- const state = parsedUrl.searchParams.get("state");
380
- const error = parsedUrl.searchParams.get("error");
381
- const result = await handleLinearCallback(code, state, error);
382
- res.writeHead(result.status, {
383
- "Content-Type": result.contentType ?? "application/json",
384
- });
385
- res.end(result.body);
386
- })();
387
- return;
388
- }
389
- if (parsedUrl.pathname === "/connections/sentry/callback" &&
390
- req.method === "GET") {
391
- void (async () => {
392
- const { handleSentryCallback } = await import("./connectors/sentry.js");
393
- const code = parsedUrl.searchParams.get("code");
394
- const state = parsedUrl.searchParams.get("state");
395
- const error = parsedUrl.searchParams.get("error");
396
- const result = await handleSentryCallback(code, state, error);
397
- res.writeHead(result.status, {
398
- "Content-Type": result.contentType ?? "application/json",
399
- });
400
- res.end(result.body);
401
- })();
307
+ // ── Connector OAuth callbacks (extracted to src/connectorRoutes.ts) ──
308
+ // Unauthenticated browser redirects from vendor must run BEFORE the
309
+ // bearer-auth gate.
310
+ if (tryHandlePublicConnectorRoute(req, res, parsedUrl)) {
402
311
  return;
403
312
  }
404
- if (parsedUrl.pathname === "/connections/google-calendar/callback" &&
405
- req.method === "GET") {
406
- void (async () => {
407
- const { handleCalendarCallback } = await import("./connectors/googleCalendar.js");
408
- const code = parsedUrl.searchParams.get("code");
409
- const state = parsedUrl.searchParams.get("state");
410
- const error = parsedUrl.searchParams.get("error");
411
- const result = await handleCalendarCallback(code, state, error);
412
- res.writeHead(result.status, {
413
- "Content-Type": result.contentType ?? "application/json",
313
+ // ── /schemas/* — unauthenticated registry-derived JSON Schemas ────────
314
+ // Serves recipe.v1.json, dry-run-plan.v1.json, tools/<ns>.json so YAML-LSP
315
+ // editors can resolve `$schema:` headers against a running bridge. No
316
+ // secrets schemas are generated from the tool registry.
317
+ if (parsedUrl.pathname?.startsWith("/schemas/") && req.method === "GET") {
318
+ try {
319
+ await import("./recipes/tools/index.js");
320
+ const { generateSchemaSet } = await import("./recipes/schemaGenerator.js");
321
+ const schemas = generateSchemaSet();
322
+ const rest = parsedUrl.pathname.slice("/schemas/".length);
323
+ let body;
324
+ if (rest === "recipe.v1.json") {
325
+ body = schemas.recipe;
326
+ }
327
+ else if (rest === "dry-run-plan.v1.json") {
328
+ body = schemas.dryRunPlan;
329
+ }
330
+ else if (rest.startsWith("tools/") && rest.endsWith(".json")) {
331
+ const ns = rest.slice("tools/".length, -".json".length);
332
+ body = schemas.namespaces[ns];
333
+ }
334
+ else if (rest === "" || rest === "index.json") {
335
+ body = {
336
+ recipe: "/schemas/recipe.v1.json",
337
+ dryRunPlan: "/schemas/dry-run-plan.v1.json",
338
+ tools: Object.keys(schemas.namespaces).map((ns) => `/schemas/tools/${ns}.json`),
339
+ };
340
+ }
341
+ if (body === undefined) {
342
+ res.writeHead(404, { "Content-Type": "application/json" });
343
+ res.end(JSON.stringify({ error: `schema not found: ${rest}` }));
344
+ return;
345
+ }
346
+ res.writeHead(200, {
347
+ "Content-Type": "application/schema+json",
348
+ "Cache-Control": "public, max-age=60",
414
349
  });
415
- res.end(result.body);
416
- })();
417
- return;
418
- }
419
- if (parsedUrl.pathname === "/connections/slack/callback" &&
420
- req.method === "GET") {
421
- void (async () => {
422
- const { handleSlackCallback } = await import("./connectors/slack.js");
423
- const code = parsedUrl.searchParams.get("code");
424
- const state = parsedUrl.searchParams.get("state");
425
- const error = parsedUrl.searchParams.get("error");
426
- const result = await handleSlackCallback(code, state, error);
427
- res.writeHead(result.status, { "Content-Type": result.contentType ?? "application/json" });
428
- res.end(result.body);
429
- })();
350
+ res.end(JSON.stringify(body, null, 2));
351
+ }
352
+ catch (err) {
353
+ respond500(res, err);
354
+ }
430
355
  return;
431
356
  }
432
357
  // ── Bearer token authentication ───────────────────────────────────────
@@ -446,8 +371,13 @@ export class Server extends EventEmitter {
446
371
  const oauthResolved = !isStaticToken && this.oauthServer
447
372
  ? this.oauthServer.resolveBearerToken(bearer)
448
373
  : null;
374
+ // Phone-path: approve/reject with x-approval-token bypass bearer check.
375
+ // The token itself is validated inside routeApprovalRequest via queue.validateToken.
376
+ const isPhoneApprovalPath = req.method === "POST" &&
377
+ /^\/(approve|reject)\/[A-Za-z0-9-]+$/.test(parsedUrl.pathname) &&
378
+ !!req.headers["x-approval-token"];
449
379
  // oauthResolved is the bridge token if the OAuth token is valid; null otherwise
450
- if (!isStaticToken && !oauthResolved) {
380
+ if (!isStaticToken && !oauthResolved && !isPhoneApprovalPath) {
451
381
  // RFC 6750: only include error= when a token was actually presented but invalid
452
382
  const tokenPresented = bearer.length > 0;
453
383
  const wwwAuth = this.oauthServer && this.oauthIssuerUrl
@@ -469,10 +399,7 @@ export class Server extends EventEmitter {
469
399
  res.end(body);
470
400
  }
471
401
  catch (err) {
472
- res.writeHead(500, { "Content-Type": "application/json" });
473
- res.end(JSON.stringify({
474
- error: err instanceof Error ? err.message : String(err),
475
- }));
402
+ respond500(res, err);
476
403
  }
477
404
  return;
478
405
  }
@@ -488,10 +415,7 @@ export class Server extends EventEmitter {
488
415
  res.end(JSON.stringify(data));
489
416
  }
490
417
  catch (err) {
491
- res.writeHead(500, { "Content-Type": "application/json" });
492
- res.end(JSON.stringify({
493
- error: err instanceof Error ? err.message : String(err),
494
- }));
418
+ respond500(res, err);
495
419
  }
496
420
  return;
497
421
  }
@@ -506,10 +430,7 @@ export class Server extends EventEmitter {
506
430
  res.end(JSON.stringify({ events: data, count: data.length }));
507
431
  }
508
432
  catch (err) {
509
- res.writeHead(500, { "Content-Type": "application/json" });
510
- res.end(JSON.stringify({
511
- error: err instanceof Error ? err.message : String(err),
512
- }));
433
+ respond500(res, err);
513
434
  }
514
435
  return;
515
436
  }
@@ -545,13 +466,82 @@ export class Server extends EventEmitter {
545
466
  res.end(JSON.stringify(data));
546
467
  }
547
468
  catch (err) {
548
- res.writeHead(500, { "Content-Type": "application/json" });
549
- res.end(JSON.stringify({
550
- error: err instanceof Error ? err.message : String(err),
551
- }));
469
+ respond500(res, err);
552
470
  }
553
471
  return;
554
472
  }
473
+ if (parsedUrl.pathname === "/traces/export" && req.method === "GET") {
474
+ void (async () => {
475
+ try {
476
+ // Accept passphrase only via header — never query string (prevents
477
+ // proxy access-log exposure and browser-history leakage).
478
+ if (parsedUrl.searchParams?.get("passphrase")) {
479
+ res.writeHead(400, { "Content-Type": "application/json" });
480
+ res.end(JSON.stringify({
481
+ error: "passphrase must be sent in the X-Trace-Passphrase header, not the URL",
482
+ }));
483
+ return;
484
+ }
485
+ const passphraseRaw = req.headers["x-trace-passphrase"] ?? null;
486
+ if (passphraseRaw !== null && passphraseRaw.length > 4096) {
487
+ res.writeHead(400, { "Content-Type": "application/json" });
488
+ res.end(JSON.stringify({
489
+ error: "passphrase too long (max 4096 chars)",
490
+ }));
491
+ return;
492
+ }
493
+ if (passphraseRaw !== null && passphraseRaw.length < 12) {
494
+ res.writeHead(400, { "Content-Type": "application/json" });
495
+ res.end(JSON.stringify({
496
+ error: "passphrase too short (min 12 chars)",
497
+ }));
498
+ return;
499
+ }
500
+ const passphrase = passphraseRaw;
501
+ const { runTracesExportToStream } = await import("./commands/tracesExport.js");
502
+ const stamp = new Date()
503
+ .toISOString()
504
+ .replace(/:/g, "-")
505
+ .replace(/\..+$/, "");
506
+ if (passphrase) {
507
+ // Encrypted export — buffer the gzip, then AES-256-GCM encrypt.
508
+ const { encryptTraceBundle } = await import("./traceEncryption.js");
509
+ const chunks = [];
510
+ const { Writable } = await import("node:stream");
511
+ const collector = new Writable({
512
+ write(chunk, _enc, cb) {
513
+ chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
514
+ cb();
515
+ },
516
+ });
517
+ await runTracesExportToStream(collector);
518
+ const plain = Buffer.concat(chunks);
519
+ const encrypted = encryptTraceBundle(plain, passphrase);
520
+ const filename = `traces-export-${stamp}.enc`;
521
+ res.writeHead(200, {
522
+ "Content-Type": "application/octet-stream",
523
+ "Content-Disposition": `attachment; filename="${filename}"`,
524
+ "Cache-Control": "no-store",
525
+ "Content-Length": String(encrypted.length),
526
+ });
527
+ res.end(encrypted);
528
+ }
529
+ else {
530
+ const filename = `traces-export-${stamp}.jsonl.gz`;
531
+ res.writeHead(200, {
532
+ "Content-Type": "application/gzip",
533
+ "Content-Disposition": `attachment; filename="${filename}"`,
534
+ "Cache-Control": "no-store",
535
+ });
536
+ await runTracesExportToStream(res);
537
+ }
538
+ }
539
+ catch (err) {
540
+ respond500(res, err);
541
+ }
542
+ })();
543
+ return;
544
+ }
555
545
  if (parsedUrl.pathname === "/analytics" && req.method === "GET") {
556
546
  try {
557
547
  const wh = parsedUrl.searchParams.get("windowHours");
@@ -570,10 +560,7 @@ export class Server extends EventEmitter {
570
560
  res.end(JSON.stringify(data));
571
561
  }
572
562
  catch (err) {
573
- res.writeHead(500, { "Content-Type": "application/json" });
574
- res.end(JSON.stringify({
575
- error: err instanceof Error ? err.message : String(err),
576
- }));
563
+ respond500(res, err);
577
564
  }
578
565
  return;
579
566
  }
@@ -587,10 +574,7 @@ export class Server extends EventEmitter {
587
574
  res.end(JSON.stringify(data));
588
575
  }
589
576
  catch (err) {
590
- res.writeHead(500, { "Content-Type": "application/json" });
591
- res.end(JSON.stringify({
592
- error: err instanceof Error ? err.message : String(err),
593
- }));
577
+ respond500(res, err);
594
578
  }
595
579
  return;
596
580
  }
@@ -618,10 +602,7 @@ export class Server extends EventEmitter {
618
602
  }
619
603
  }
620
604
  catch (err) {
621
- res.writeHead(500, { "Content-Type": "application/json" });
622
- res.end(JSON.stringify({
623
- error: err instanceof Error ? err.message : String(err),
624
- }));
605
+ respond500(res, err);
625
606
  }
626
607
  return;
627
608
  }
@@ -674,570 +655,247 @@ export class Server extends EventEmitter {
674
655
  res.end(JSON.stringify(data));
675
656
  }
676
657
  catch (err) {
677
- res.writeHead(500, { "Content-Type": "application/json" });
678
- res.end(JSON.stringify({
679
- error: err instanceof Error ? err.message : String(err),
680
- }));
658
+ respond500(res, err);
681
659
  }
682
660
  return;
683
661
  }
684
- if (parsedUrl.pathname?.startsWith("/hooks/") && req.method === "POST") {
685
- const hookPath = parsedUrl.pathname.substring("/hooks".length);
686
- const chunks = [];
687
- req.on("data", (c) => chunks.push(c));
688
- req.on("end", () => {
689
- void (async () => {
690
- let payload;
691
- if (chunks.length > 0) {
692
- const body = Buffer.concat(chunks).toString("utf-8");
693
- if (body.trim()) {
694
- try {
695
- payload = JSON.parse(body);
696
- }
697
- catch {
698
- payload = body;
699
- }
700
- }
701
- }
702
- if (!this.webhookFn) {
703
- res.writeHead(503, { "Content-Type": "application/json" });
704
- res.end(JSON.stringify({
705
- ok: false,
706
- error: "Webhooks unavailable — start bridge with --claude-driver subprocess",
707
- }));
708
- return;
709
- }
710
- const result = await this.webhookFn(hookPath, payload);
711
- const status = result.ok
712
- ? 200
713
- : result.error === "not_found"
714
- ? 404
715
- : 400;
716
- res.writeHead(status, { "Content-Type": "application/json" });
717
- res.end(JSON.stringify(result));
718
- })();
719
- });
720
- return;
721
- }
722
- // ── Gmail / Connections endpoints ───────────────────────────────────────
723
- if (parsedUrl.pathname === "/connections" && req.method === "GET") {
724
- void (async () => {
725
- const { handleConnectionsList } = await import("./connectors/gmail.js");
726
- const result = await handleConnectionsList();
727
- res.writeHead(result.status, {
728
- "Content-Type": result.contentType ?? "application/json",
729
- });
730
- res.end(result.body);
731
- })();
732
- return;
733
- }
734
- if (parsedUrl.pathname === "/connections/gmail/auth" &&
735
- req.method === "GET") {
736
- void (async () => {
737
- const { handleGmailAuthRedirect } = await import("./connectors/gmail.js");
738
- const result = handleGmailAuthRedirect();
739
- if (result.redirect) {
740
- res.writeHead(302, { Location: result.redirect });
741
- res.end();
662
+ const cancelMatch = parsedUrl.pathname?.match(/^\/tasks\/([^/]+)\/cancel$/);
663
+ if (cancelMatch && req.method === "POST") {
664
+ const taskId = cancelMatch[1];
665
+ try {
666
+ const found = this.cancelTaskFn?.(taskId) ?? false;
667
+ if (!found) {
668
+ res.writeHead(404, { "Content-Type": "application/json" });
669
+ res.end(JSON.stringify({ error: "task not found or already terminal" }));
742
670
  }
743
671
  else {
744
- res.writeHead(result.status, {
745
- "Content-Type": result.contentType ?? "application/json",
746
- });
747
- res.end(result.body);
672
+ res.writeHead(200, { "Content-Type": "application/json" });
673
+ res.end(JSON.stringify({ ok: true }));
748
674
  }
749
- })();
750
- return;
751
- }
752
- if (parsedUrl.pathname === "/connections/gmail/callback" &&
753
- req.method === "GET") {
754
- void (async () => {
755
- const { handleGmailCallback } = await import("./connectors/gmail.js");
756
- const code = parsedUrl.searchParams.get("code");
757
- const state = parsedUrl.searchParams.get("state");
758
- const error = parsedUrl.searchParams.get("error");
759
- const result = await handleGmailCallback(code, state, error);
760
- res.writeHead(result.status, {
761
- "Content-Type": result.contentType ?? "text/html",
762
- });
763
- res.end(result.body);
764
- })();
765
- return;
766
- }
767
- if (parsedUrl.pathname === "/connections/gmail" &&
768
- req.method === "DELETE") {
769
- void (async () => {
770
- const { handleGmailDisconnect } = await import("./connectors/gmail.js");
771
- const result = await handleGmailDisconnect();
772
- res.writeHead(result.status, {
773
- "Content-Type": result.contentType ?? "application/json",
774
- });
775
- res.end(result.body);
776
- })();
675
+ }
676
+ catch (err) {
677
+ respond500(res, err);
678
+ }
777
679
  return;
778
680
  }
779
- if (parsedUrl.pathname === "/connections/gmail/test" &&
780
- req.method === "POST") {
781
- void (async () => {
782
- const { handleGmailTest } = await import("./connectors/gmail.js");
783
- const result = await handleGmailTest();
784
- res.writeHead(result.status, {
785
- "Content-Type": result.contentType ?? "application/json",
681
+ if (parsedUrl.pathname?.startsWith("/hooks/") && req.method === "POST") {
682
+ const hookPath = parsedUrl.pathname.substring("/hooks".length);
683
+ // 256 KB webhook payloads from GitHub/Linear/etc. are typically
684
+ // 1–25 KB; 256 KB matches RECIPE_ROUTE_BODY_CAPS.content as a
685
+ // generous ceiling that still bounds an authenticated DoS vector.
686
+ const HOOKS_BODY_CAP = 256 * 1024;
687
+ const read = await readBodyWithCap(req, HOOKS_BODY_CAP);
688
+ if (!read.ok) {
689
+ respond413(res, HOOKS_BODY_CAP);
690
+ return;
691
+ }
692
+ let payload;
693
+ if (read.body.trim()) {
694
+ try {
695
+ payload = JSON.parse(read.body);
696
+ }
697
+ catch {
698
+ payload = read.body;
699
+ }
700
+ }
701
+ if (!this.webhookFn) {
702
+ res.writeHead(503, { "Content-Type": "application/json" });
703
+ res.end(JSON.stringify({
704
+ ok: false,
705
+ error: "Webhooks unavailable — start bridge with --claude-driver subprocess",
706
+ }));
707
+ return;
708
+ }
709
+ const result = await this.webhookFn(hookPath, payload);
710
+ const status = result.ok
711
+ ? 200
712
+ : result.error === "not_found"
713
+ ? 404
714
+ : 400;
715
+ // Record in ring buffer so the dashboard can show "last
716
+ // payload" per recipe. Skip not_found so unknown paths don't
717
+ // pollute the buffer with garbage / scanner traffic.
718
+ if (result.error !== "not_found") {
719
+ const existing = this.webhookPayloads.get(hookPath) ?? [];
720
+ existing.unshift({
721
+ receivedAt: Date.now(),
722
+ payload,
723
+ ok: result.ok,
724
+ error: result.error,
725
+ taskId: result.taskId,
726
+ recipeName: result.name,
786
727
  });
787
- res.end(result.body);
788
- })();
728
+ if (existing.length > Server.MAX_WEBHOOK_PAYLOADS) {
729
+ existing.length = Server.MAX_WEBHOOK_PAYLOADS;
730
+ }
731
+ this.webhookPayloads.set(hookPath, existing);
732
+ }
733
+ res.writeHead(status, { "Content-Type": "application/json" });
734
+ res.end(JSON.stringify(result));
789
735
  return;
790
736
  }
791
- // ── GitHub MCP connector routes ─────────────────────────────────────
792
- if (parsedUrl.pathname === "/connections/github/auth" &&
737
+ if (parsedUrl.pathname?.startsWith("/webhook-payloads/") &&
793
738
  req.method === "GET") {
794
- void (async () => {
795
- const { handleGithubAuthorize } = await import("./connectors/github.js");
796
- const result = await handleGithubAuthorize();
797
- if (result.redirect) {
798
- res.writeHead(302, { Location: result.redirect });
799
- res.end();
800
- }
801
- else {
802
- res.writeHead(result.status, {
803
- "Content-Type": result.contentType ?? "application/json",
804
- });
805
- res.end(result.body);
806
- }
807
- })();
739
+ const hookPath = parsedUrl.pathname.substring("/webhook-payloads".length);
740
+ const entries = this.webhookPayloads.get(hookPath) ?? [];
741
+ res.writeHead(200, { "Content-Type": "application/json" });
742
+ res.end(JSON.stringify({ path: hookPath, entries }));
808
743
  return;
809
744
  }
810
- if (parsedUrl.pathname === "/connections/github/test" &&
811
- req.method === "POST") {
812
- void (async () => {
813
- const { handleGithubTest } = await import("./connectors/github.js");
814
- const result = await handleGithubTest();
815
- res.writeHead(result.status, {
816
- "Content-Type": result.contentType ?? "application/json",
817
- });
818
- res.end(result.body);
819
- })();
745
+ // Activity-based automation suggestions (Phase 3 §4). Read-only
746
+ // pattern-mining over the running bridge's activity log + recipe
747
+ // run history. Same logic the `patchwork suggest` CLI calls — this
748
+ // exposes it to the dashboard so suggestions live where users look.
749
+ if (parsedUrl.pathname === "/suggestions" && req.method === "GET") {
750
+ if (!this.activityLog) {
751
+ res.writeHead(503, { "Content-Type": "application/json" });
752
+ res.end(JSON.stringify({
753
+ error: "activity log not wired — bridge probably not in a configuration that records activity",
754
+ }));
755
+ return;
756
+ }
757
+ const sinceDaysParam = parsedUrl.searchParams?.get("sinceDays");
758
+ const sinceDays = sinceDaysParam !== null && sinceDaysParam !== undefined
759
+ ? Number.parseInt(sinceDaysParam, 10)
760
+ : undefined;
761
+ const { computeAutomationSuggestions } = await import("./automationSuggestions.js");
762
+ const opts = {
763
+ activityLog: this.activityLog,
764
+ };
765
+ if (this.recipeRunLog)
766
+ opts.recipeRunLog = this.recipeRunLog;
767
+ if (sinceDays !== undefined && Number.isFinite(sinceDays)) {
768
+ opts.activitySinceMs = sinceDays * 24 * 60 * 60 * 1000;
769
+ }
770
+ const suggestions = computeAutomationSuggestions(opts);
771
+ res.writeHead(200, { "Content-Type": "application/json" });
772
+ res.end(JSON.stringify({
773
+ suggestions,
774
+ generatedAt: new Date().toISOString(),
775
+ }));
820
776
  return;
821
777
  }
822
- if (parsedUrl.pathname === "/connections/github" &&
823
- req.method === "DELETE") {
824
- void (async () => {
825
- const { handleGithubDisconnect } = await import("./connectors/github.js");
826
- const result = await handleGithubDisconnect();
827
- res.writeHead(result.status, {
828
- "Content-Type": result.contentType ?? "application/json",
829
- });
830
- res.end(result.body);
831
- })();
778
+ // Approval insights aggregate approval-decision history for Phase 3 §3
779
+ // passive risk personalization. Read-only; no state changes.
780
+ if (parsedUrl.pathname === "/approval-insights" && req.method === "GET") {
781
+ if (!this.activityLog) {
782
+ res.writeHead(503, { "Content-Type": "application/json" });
783
+ res.end(JSON.stringify({
784
+ error: "activity log not wired",
785
+ }));
786
+ return;
787
+ }
788
+ const { computeApprovalInsights } = await import("./approvalInsights.js");
789
+ const result = computeApprovalInsights(this.activityLog);
790
+ res.writeHead(200, { "Content-Type": "application/json" });
791
+ res.end(JSON.stringify(result));
832
792
  return;
833
793
  }
834
- // ── Sentry MCP connector routes ─────────────────────────────────────
835
- if (parsedUrl.pathname === "/connections/sentry/auth" &&
794
+ // Decision replay Phase 3 §2. Re-evaluates historical approval
795
+ // decisions against the current CC policy. Read-only; no side effects.
796
+ if (parsedUrl.pathname === "/approval-insights/replay" &&
836
797
  req.method === "GET") {
837
- void (async () => {
838
- const { handleSentryAuthorize } = await import("./connectors/sentry.js");
839
- const result = await handleSentryAuthorize();
840
- if (result.redirect) {
841
- res.writeHead(302, { Location: result.redirect });
842
- res.end();
843
- }
844
- else {
845
- res.writeHead(result.status, {
846
- "Content-Type": result.contentType ?? "application/json",
847
- });
848
- res.end(result.body);
849
- }
850
- })();
798
+ if (!this.activityLog) {
799
+ res.writeHead(503, { "Content-Type": "application/json" });
800
+ res.end(JSON.stringify({ error: "activity log not wired" }));
801
+ return;
802
+ }
803
+ const sinceDaysParam = parsedUrl.searchParams?.get("sinceDays");
804
+ const sinceDays = sinceDaysParam !== null && sinceDaysParam !== undefined
805
+ ? Number.parseInt(sinceDaysParam, 10)
806
+ : 7;
807
+ const sinceMs = Number.isFinite(sinceDays)
808
+ ? Date.now() - sinceDays * 24 * 60 * 60 * 1000
809
+ : 0;
810
+ const { computeDecisionReplay } = await import("./decisionReplay.js");
811
+ const result = computeDecisionReplay(this.activityLog, {
812
+ workspace: process.cwd(),
813
+ sinceMs,
814
+ });
815
+ res.writeHead(200, { "Content-Type": "application/json" });
816
+ res.end(JSON.stringify(result));
851
817
  return;
852
818
  }
853
- if (parsedUrl.pathname === "/connections/sentry/callback" &&
819
+ // Rule explanation returns which CC permission rule matched a tool call
820
+ // and why approval was required. Phase 1 §2 Delegation Policy UX.
821
+ if (parsedUrl.pathname === "/approval-insights/explain" &&
854
822
  req.method === "GET") {
855
- void (async () => {
856
- const { handleSentryCallback } = await import("./connectors/sentry.js");
857
- const code = parsedUrl.searchParams.get("code");
858
- const state = parsedUrl.searchParams.get("state");
859
- const error = parsedUrl.searchParams.get("error");
860
- const result = await handleSentryCallback(code, state, error);
861
- res.writeHead(result.status, {
862
- "Content-Type": result.contentType ?? "application/json",
863
- });
864
- res.end(result.body);
865
- })();
823
+ const tool = parsedUrl.searchParams?.get("tool") ?? "";
824
+ const specifier = parsedUrl.searchParams?.get("specifier") ?? undefined;
825
+ if (!tool) {
826
+ res.writeHead(400, { "Content-Type": "application/json" });
827
+ res.end(JSON.stringify({ error: "tool param required" }));
828
+ return;
829
+ }
830
+ const { loadCcPermissionsAttributed, explainRules } = await import("./ccPermissions.js");
831
+ // Cache rules for 30 s — loadCcPermissionsAttributed walks the
832
+ // filesystem and this endpoint can be polled by the dashboard.
833
+ const now = Date.now();
834
+ if (!this._explainRulesCache ||
835
+ now - this._explainRulesCache.at > 30_000) {
836
+ this._explainRulesCache = {
837
+ at: now,
838
+ rules: loadCcPermissionsAttributed(process.cwd()),
839
+ };
840
+ }
841
+ const explanation = explainRules(tool, specifier || undefined, this._explainRulesCache.rules);
842
+ res.writeHead(200, { "Content-Type": "application/json" });
843
+ res.end(JSON.stringify({ tool, specifier: specifier ?? null, explanation }));
866
844
  return;
867
845
  }
868
- if (parsedUrl.pathname === "/connections/sentry/test" &&
846
+ // Reversible-refactoring surface list active staged transactions
847
+ // (Phase 1 §3 dashboard ask). Read-only metadata for the dashboard
848
+ // /transactions page; no file contents leave the bridge.
849
+ if (parsedUrl.pathname === "/transactions" && req.method === "GET") {
850
+ const { listActiveTransactions } = await import("./tools/transaction.js");
851
+ const transactions = listActiveTransactions();
852
+ res.writeHead(200, { "Content-Type": "application/json" });
853
+ res.end(JSON.stringify({ transactions }));
854
+ return;
855
+ }
856
+ // Discard-only — we deliberately do NOT expose commit via HTTP because
857
+ // the commit handler needs per-workspace context wired through
858
+ // createTransactionTools(). Rollback is pure-memory and workspace-
859
+ // agnostic, safe to expose. Commit from the agent side via MCP.
860
+ if (parsedUrl.pathname?.match(/^\/transactions\/[^/]+\/rollback$/) &&
869
861
  req.method === "POST") {
870
- void (async () => {
871
- const { handleSentryTest } = await import("./connectors/sentry.js");
872
- const result = await handleSentryTest();
873
- res.writeHead(result.status, {
874
- "Content-Type": result.contentType ?? "application/json",
875
- });
876
- res.end(result.body);
877
- })();
878
- return;
879
- }
880
- if (parsedUrl.pathname === "/connections/sentry" &&
881
- req.method === "DELETE") {
882
- void (async () => {
883
- const { handleSentryDisconnect } = await import("./connectors/sentry.js");
884
- const result = await handleSentryDisconnect();
885
- res.writeHead(result.status, {
886
- "Content-Type": result.contentType ?? "application/json",
887
- });
888
- res.end(result.body);
889
- })();
890
- return;
891
- }
892
- // ── Linear MCP connector routes ─────────────────────────────────────
893
- if (parsedUrl.pathname === "/connections/linear/auth" &&
894
- req.method === "GET") {
895
- void (async () => {
896
- const { handleLinearAuthorize } = await import("./connectors/linear.js");
897
- const result = await handleLinearAuthorize();
898
- if (result.redirect) {
899
- res.writeHead(302, { Location: result.redirect });
900
- res.end();
901
- }
902
- else {
903
- res.writeHead(result.status, {
904
- "Content-Type": result.contentType ?? "application/json",
905
- });
906
- res.end(result.body);
907
- }
908
- })();
909
- return;
910
- }
911
- if (parsedUrl.pathname === "/connections/linear/callback" &&
912
- req.method === "GET") {
913
- void (async () => {
914
- const { handleLinearCallback } = await import("./connectors/linear.js");
915
- const code = parsedUrl.searchParams.get("code");
916
- const state = parsedUrl.searchParams.get("state");
917
- const error = parsedUrl.searchParams.get("error");
918
- const result = await handleLinearCallback(code, state, error);
919
- res.writeHead(result.status, {
920
- "Content-Type": result.contentType ?? "application/json",
921
- });
922
- res.end(result.body);
923
- })();
924
- return;
925
- }
926
- if (parsedUrl.pathname === "/connections/linear/test" &&
927
- req.method === "POST") {
928
- void (async () => {
929
- const { handleLinearTest } = await import("./connectors/linear.js");
930
- const result = await handleLinearTest();
931
- res.writeHead(result.status, {
932
- "Content-Type": result.contentType ?? "application/json",
933
- });
934
- res.end(result.body);
935
- })();
936
- return;
937
- }
938
- if (parsedUrl.pathname === "/connections/linear" &&
939
- req.method === "DELETE") {
940
- void (async () => {
941
- const { handleLinearDisconnect } = await import("./connectors/linear.js");
942
- const result = await handleLinearDisconnect();
943
- res.writeHead(result.status, {
944
- "Content-Type": result.contentType ?? "application/json",
945
- });
946
- res.end(result.body);
947
- })();
948
- return;
949
- }
950
- // ── Slack connector routes ──────────────────────────────────────
951
- if ((parsedUrl.pathname === "/connections/slack/auth" || parsedUrl.pathname === "/connections/slack/authorize") &&
952
- req.method === "GET") {
953
- const { handleSlackAuthorize } = await import("./connectors/slack.js");
954
- const result = handleSlackAuthorize();
955
- if (result.redirect) {
956
- res.writeHead(302, { Location: result.redirect });
957
- res.end();
958
- }
959
- else {
960
- res.writeHead(result.status, { "Content-Type": result.contentType ?? "application/json" });
961
- res.end(result.body);
962
- }
963
- return;
964
- }
965
- if (parsedUrl.pathname === "/connections/slack/test" &&
966
- req.method === "POST") {
967
- void (async () => {
968
- const { handleSlackTest } = await import("./connectors/slack.js");
969
- const result = await handleSlackTest();
970
- res.writeHead(result.status, { "Content-Type": result.contentType ?? "application/json" });
971
- res.end(result.body);
972
- })();
973
- return;
974
- }
975
- if (parsedUrl.pathname === "/connections/slack" &&
976
- req.method === "DELETE") {
977
- const { handleSlackDisconnect } = await import("./connectors/slack.js");
978
- const result = handleSlackDisconnect();
979
- res.writeHead(result.status, { "Content-Type": result.contentType ?? "application/json" });
980
- res.end(result.body);
981
- return;
982
- }
983
- // ── Google Calendar routes ──────────────────────────────────────
984
- if (parsedUrl.pathname === "/connections/google-calendar/auth" &&
985
- req.method === "GET") {
986
- void (async () => {
987
- const { handleCalendarAuthRedirect } = await import("./connectors/googleCalendar.js");
988
- const result = handleCalendarAuthRedirect();
989
- if (result.redirect) {
990
- res.writeHead(302, { Location: result.redirect });
991
- res.end();
992
- }
993
- else {
994
- res.writeHead(result.status, {
995
- "Content-Type": result.contentType ?? "application/json",
996
- });
997
- res.end(result.body);
998
- }
999
- })();
1000
- return;
1001
- }
1002
- if (parsedUrl.pathname === "/connections/google-calendar/callback" &&
1003
- req.method === "GET") {
1004
- void (async () => {
1005
- const { handleCalendarCallback } = await import("./connectors/googleCalendar.js");
1006
- const code = parsedUrl.searchParams.get("code");
1007
- const state = parsedUrl.searchParams.get("state");
1008
- const error = parsedUrl.searchParams.get("error");
1009
- const result = await handleCalendarCallback(code, state, error);
1010
- res.writeHead(result.status, {
1011
- "Content-Type": result.contentType ?? "application/json",
1012
- });
1013
- res.end(result.body);
1014
- })();
1015
- return;
1016
- }
1017
- if (parsedUrl.pathname === "/connections/google-calendar/test" &&
1018
- req.method === "POST") {
1019
- void (async () => {
1020
- const { handleCalendarTest } = await import("./connectors/googleCalendar.js");
1021
- const result = await handleCalendarTest();
1022
- res.writeHead(result.status, {
1023
- "Content-Type": result.contentType ?? "application/json",
1024
- });
1025
- res.end(result.body);
1026
- })();
1027
- return;
1028
- }
1029
- if (parsedUrl.pathname === "/connections/google-calendar" &&
1030
- req.method === "DELETE") {
1031
- void (async () => {
1032
- const { handleCalendarDisconnect } = await import("./connectors/googleCalendar.js");
1033
- const result = await handleCalendarDisconnect();
1034
- res.writeHead(result.status, {
1035
- "Content-Type": result.contentType ?? "application/json",
1036
- });
1037
- res.end(result.body);
1038
- })();
1039
- return;
1040
- }
1041
- // ── Inbox routes ────────────────────────────────────────────────────
1042
- if (parsedUrl.pathname === "/inbox" && req.method === "GET") {
1043
- void (async () => {
1044
- try {
1045
- const { readdir, readFile, stat } = await import("node:fs/promises");
1046
- const { existsSync } = await import("node:fs");
1047
- const inboxDir = path.join(os.homedir(), ".patchwork", "inbox");
1048
- if (!existsSync(inboxDir)) {
1049
- res.writeHead(200, { "Content-Type": "application/json" });
1050
- res.end(JSON.stringify({ items: [] }));
1051
- return;
1052
- }
1053
- const files = (await readdir(inboxDir)).filter((f) => f.endsWith(".md"));
1054
- const items = await Promise.all(files.map(async (name) => {
1055
- const filePath = path.join(inboxDir, name);
1056
- const [content, stats] = await Promise.all([
1057
- readFile(filePath, "utf8"),
1058
- stat(filePath),
1059
- ]);
1060
- const stripped = content
1061
- .split("\n")
1062
- .filter((l) => !l.startsWith("#"))
1063
- .join("\n")
1064
- .trim();
1065
- return {
1066
- name,
1067
- path: filePath,
1068
- modifiedAt: stats.mtime.toISOString(),
1069
- preview: stripped.slice(0, 200),
1070
- };
1071
- }));
1072
- items.sort((a, b) => new Date(b.modifiedAt).getTime() -
1073
- new Date(a.modifiedAt).getTime());
1074
- res.writeHead(200, { "Content-Type": "application/json" });
1075
- res.end(JSON.stringify({ items }));
1076
- }
1077
- catch (err) {
1078
- res.writeHead(500, { "Content-Type": "application/json" });
1079
- res.end(JSON.stringify({
1080
- error: err instanceof Error ? err.message : String(err),
1081
- }));
1082
- }
1083
- })();
1084
- return;
1085
- }
1086
- const inboxFileMatch = parsedUrl.pathname?.match(/^\/inbox\/([^/]+\.md)$/);
1087
- if (inboxFileMatch && req.method === "GET") {
1088
- void (async () => {
1089
- try {
1090
- const { readFile, stat } = await import("node:fs/promises");
1091
- const filename = decodeURIComponent(inboxFileMatch[1] ?? "");
1092
- // Prevent path traversal — filename must not contain directory separators
1093
- if (filename.includes("/") || filename.includes("\\")) {
1094
- res.writeHead(400, { "Content-Type": "application/json" });
1095
- res.end(JSON.stringify({ error: "Invalid filename" }));
1096
- return;
1097
- }
1098
- const filePath = path.join(os.homedir(), ".patchwork", "inbox", filename);
1099
- const [content, stats] = await Promise.all([
1100
- readFile(filePath, "utf8"),
1101
- stat(filePath),
1102
- ]);
1103
- res.writeHead(200, { "Content-Type": "application/json" });
1104
- res.end(JSON.stringify({
1105
- name: filename,
1106
- content,
1107
- modifiedAt: stats.mtime.toISOString(),
1108
- }));
1109
- }
1110
- catch (err) {
1111
- const code = err.code;
1112
- if (code === "ENOENT") {
1113
- res.writeHead(404, { "Content-Type": "application/json" });
1114
- res.end(JSON.stringify({ error: "Not found" }));
1115
- }
1116
- else {
1117
- res.writeHead(500, { "Content-Type": "application/json" });
1118
- res.end(JSON.stringify({
1119
- error: err instanceof Error ? err.message : String(err),
1120
- }));
1121
- }
1122
- }
1123
- })();
1124
- return;
1125
- }
1126
- // ── End inbox routes ─────────────────────────────────────────────────
1127
- if (parsedUrl.pathname === "/recipes/run" && req.method === "POST") {
1128
- const chunks = [];
1129
- req.on("data", (c) => chunks.push(c));
1130
- req.on("end", () => {
1131
- void (async () => {
1132
- try {
1133
- const body = Buffer.concat(chunks).toString("utf-8");
1134
- const parsed = JSON.parse(body || "{}");
1135
- const name = parsed.name;
1136
- const vars = parsed.vars &&
1137
- typeof parsed.vars === "object" &&
1138
- !Array.isArray(parsed.vars)
1139
- ? parsed.vars
1140
- : undefined;
1141
- if (typeof name !== "string" || !name) {
1142
- res.writeHead(400, { "Content-Type": "application/json" });
1143
- res.end(JSON.stringify({ ok: false, error: "name required" }));
1144
- return;
1145
- }
1146
- if (!this.runRecipeFn) {
1147
- res.writeHead(503, { "Content-Type": "application/json" });
1148
- res.end(JSON.stringify({
1149
- ok: false,
1150
- error: "Recipe execution unavailable — requires --claude-driver subprocess",
1151
- }));
1152
- return;
1153
- }
1154
- const result = await this.runRecipeFn(name, vars);
1155
- res.writeHead(result.ok ? 200 : 400, {
1156
- "Content-Type": "application/json",
1157
- });
1158
- res.end(JSON.stringify(result));
1159
- }
1160
- catch {
1161
- res.writeHead(400, { "Content-Type": "application/json" });
1162
- res.end(JSON.stringify({ ok: false, error: "Invalid JSON body" }));
1163
- }
1164
- })();
1165
- });
1166
- return;
1167
- }
1168
- if (parsedUrl.pathname === "/runs" && req.method === "GET") {
1169
- try {
1170
- const sp = parsedUrl.searchParams;
1171
- const limitRaw = sp.get("limit");
1172
- const afterRaw = sp.get("after");
1173
- const trigger = sp.get("trigger");
1174
- const status = sp.get("status");
1175
- const recipe = sp.get("recipe");
1176
- const limit = limitRaw ? Number.parseInt(limitRaw, 10) : Number.NaN;
1177
- const after = afterRaw ? Number.parseInt(afterRaw, 10) : Number.NaN;
1178
- const runs = this.runsFn?.({
1179
- ...(Number.isFinite(limit) && { limit }),
1180
- ...(trigger && { trigger }),
1181
- ...(status && { status }),
1182
- ...(recipe && { recipe }),
1183
- ...(Number.isFinite(after) && { after }),
1184
- }) ?? [];
1185
- res.writeHead(200, { "Content-Type": "application/json" });
1186
- res.end(JSON.stringify({ runs }));
1187
- }
1188
- catch (err) {
1189
- res.writeHead(500, { "Content-Type": "application/json" });
1190
- res.end(JSON.stringify({
1191
- error: err instanceof Error ? err.message : String(err),
1192
- }));
1193
- }
1194
- return;
1195
- }
1196
- if (req.url === "/recipes" && req.method === "POST") {
1197
- const chunks = [];
1198
- req.on("data", (c) => chunks.push(c));
1199
- req.on("end", () => {
1200
- try {
1201
- const body = Buffer.concat(chunks).toString("utf-8");
1202
- const draft = JSON.parse(body || "{}");
1203
- if (typeof draft.name !== "string" || !draft.name) {
1204
- res.writeHead(400, { "Content-Type": "application/json" });
1205
- res.end(JSON.stringify({ ok: false, error: "name required" }));
1206
- return;
1207
- }
1208
- if (!this.saveRecipeFn) {
1209
- res.writeHead(503, { "Content-Type": "application/json" });
1210
- res.end(JSON.stringify({
1211
- ok: false,
1212
- error: "Recipe saving unavailable",
1213
- }));
1214
- return;
1215
- }
1216
- const result = this.saveRecipeFn(draft);
1217
- res.writeHead(result.ok ? 201 : 400, {
1218
- "Content-Type": "application/json",
1219
- });
1220
- res.end(JSON.stringify(result));
1221
- }
1222
- catch {
1223
- res.writeHead(400, { "Content-Type": "application/json" });
1224
- res.end(JSON.stringify({ ok: false, error: "Invalid JSON body" }));
1225
- }
1226
- });
1227
- return;
1228
- }
1229
- if (req.url === "/recipes" && req.method === "GET") {
1230
- try {
1231
- const data = this.recipesFn?.() ?? { recipesDir: null, recipes: [] };
1232
- res.writeHead(200, { "Content-Type": "application/json" });
1233
- res.end(JSON.stringify(data));
1234
- }
1235
- catch (err) {
1236
- res.writeHead(500, { "Content-Type": "application/json" });
1237
- res.end(JSON.stringify({
1238
- error: err instanceof Error ? err.message : String(err),
1239
- }));
1240
- }
862
+ const id = parsedUrl.pathname.split("/")[2] ?? "";
863
+ const { rollbackTransactionById } = await import("./tools/transaction.js");
864
+ const ok = id !== "" && rollbackTransactionById(id);
865
+ res.writeHead(ok ? 200 : 404, { "Content-Type": "application/json" });
866
+ res.end(JSON.stringify(ok
867
+ ? { ok: true, transactionId: id }
868
+ : { ok: false, error: "transaction not found" }));
869
+ return;
870
+ }
871
+ // ── Connector routes (extracted to src/connectorRoutes.ts) ──────────
872
+ if (tryHandleConnectorRoute(req, res, parsedUrl)) {
873
+ return;
874
+ }
875
+ // ── Inbox routes (extracted to src/inboxRoutes.ts) ───────────────────
876
+ if (tryHandleInboxRoute(req, res, parsedUrl)) {
877
+ return;
878
+ }
879
+ // ── Recipe / runs / templates routes (extracted to src/recipeRoutes.ts)
880
+ if (tryHandleRecipeRoute(req, res, parsedUrl, {
881
+ setRecipeTrustFn: this.setRecipeTrustFn,
882
+ generateRecipeFn: this.generateRecipeFn,
883
+ recipesFn: this.recipesFn,
884
+ loadRecipeContentFn: this.loadRecipeContentFn,
885
+ saveRecipeContentFn: this.saveRecipeContentFn,
886
+ deleteRecipeContentFn: this.deleteRecipeContentFn,
887
+ archiveRecipeFn: this.archiveRecipeFn,
888
+ duplicateRecipeFn: this.duplicateRecipeFn,
889
+ promoteRecipeVariantFn: this.promoteRecipeVariantFn,
890
+ lintRecipeContentFn: this.lintRecipeContentFn,
891
+ saveRecipeFn: this.saveRecipeFn,
892
+ setRecipeEnabledFn: this.setRecipeEnabledFn,
893
+ runsFn: this.runsFn,
894
+ runDetailFn: this.runDetailFn,
895
+ runPlanFn: this.runPlanFn,
896
+ runReplayFn: this.runReplayFn,
897
+ runRecipeFn: this.runRecipeFn,
898
+ })) {
1241
899
  return;
1242
900
  }
1243
901
  const sessionDetailMatch = /^\/sessions\/([A-Za-z0-9-]+)$/.exec(parsedUrl.pathname);
@@ -1254,10 +912,7 @@ export class Server extends EventEmitter {
1254
912
  res.end(JSON.stringify(data));
1255
913
  }
1256
914
  catch (err) {
1257
- res.writeHead(500, { "Content-Type": "application/json" });
1258
- res.end(JSON.stringify({
1259
- error: err instanceof Error ? err.message : String(err),
1260
- }));
915
+ respond500(res, err);
1261
916
  }
1262
917
  return;
1263
918
  }
@@ -1273,21 +928,33 @@ export class Server extends EventEmitter {
1273
928
  res.end(JSON.stringify(data));
1274
929
  }
1275
930
  catch (err) {
1276
- res.writeHead(500, { "Content-Type": "application/json" });
1277
- res.end(JSON.stringify({
1278
- error: err instanceof Error ? err.message : String(err),
1279
- }));
931
+ respond500(res, err);
1280
932
  }
1281
933
  return;
1282
934
  }
1283
935
  if (parsedUrl.pathname === "/settings" && req.method === "POST") {
1284
- const chunks = [];
1285
- req.on("data", (c) => chunks.push(c));
1286
- req.on("end", () => {
1287
- try {
1288
- const body = JSON.parse(Buffer.concat(chunks).toString("utf-8"));
1289
- const raw = body.webhookUrl?.trim() ?? "";
1290
- if (raw !== "" && !/^https:\/\/.+/.test(raw)) {
936
+ // 16 KB — settings POSTs are short-string fields (URLs, API keys,
937
+ // gate level). 16 KB is generous; an authenticated attacker can't
938
+ // stream gigabytes here.
939
+ const SETTINGS_BODY_CAP = 16 * 1024;
940
+ const parsed = await readJsonBody(req, SETTINGS_BODY_CAP);
941
+ if (!parsed.ok) {
942
+ if (parsed.code === "too_large") {
943
+ respond413(res, SETTINGS_BODY_CAP);
944
+ return;
945
+ }
946
+ res.writeHead(400, { "Content-Type": "application/json" });
947
+ res.end(JSON.stringify({ error: "Invalid request body" }));
948
+ return;
949
+ }
950
+ try {
951
+ {
952
+ const body = parsed.value ?? {};
953
+ const hasWebhookUpdate = body.webhookUrl !== undefined;
954
+ const raw = hasWebhookUpdate
955
+ ? (body.webhookUrl?.trim() ?? "")
956
+ : undefined;
957
+ if (raw !== undefined && raw !== "" && !/^https:\/\/.+/.test(raw)) {
1291
958
  res.writeHead(400, { "Content-Type": "application/json" });
1292
959
  res.end(JSON.stringify({ error: "webhookUrl must be HTTPS" }));
1293
960
  return;
@@ -1306,58 +973,187 @@ export class Server extends EventEmitter {
1306
973
  const configPath = patchworkConfigPath();
1307
974
  const cfg = loadPatchworkConfig(configPath);
1308
975
  cfg.dashboard = {
1309
- port: cfg.dashboard?.port ?? 3000,
976
+ port: cfg.dashboard?.port ?? 3200,
1310
977
  requireApproval: cfg.dashboard?.requireApproval ?? ["high"],
1311
978
  pushNotifications: cfg.dashboard?.pushNotifications ?? false,
1312
- webhookUrl: raw || undefined,
979
+ webhookUrl: hasWebhookUpdate
980
+ ? raw || undefined
981
+ : cfg.dashboard?.webhookUrl,
1313
982
  };
1314
983
  if (gateRaw !== undefined) {
1315
984
  cfg.approvalGate = gateRaw;
1316
985
  this.approvalGate = gateRaw;
1317
986
  }
1318
- savePatchworkConfig(cfg, configPath);
1319
- this.approvalWebhookUrl = raw || undefined;
987
+ // h10 toggle: must be boolean if present. Persists to
988
+ // ~/.patchwork/config.json AND live-mutates the Server
989
+ // field so the next /approvals POST honors it without
990
+ // needing a bridge restart.
991
+ if (body.enableTimeOfDayAnomaly !== undefined) {
992
+ if (typeof body.enableTimeOfDayAnomaly !== "boolean") {
993
+ res.writeHead(400, { "Content-Type": "application/json" });
994
+ res.end(JSON.stringify({
995
+ error: "enableTimeOfDayAnomaly must be a boolean",
996
+ }));
997
+ return;
998
+ }
999
+ cfg.enableTimeOfDayAnomaly = body.enableTimeOfDayAnomaly;
1000
+ this.enableTimeOfDayAnomaly = body.enableTimeOfDayAnomaly;
1001
+ }
1002
+ const driverRaw = body.driver;
1003
+ if (driverRaw !== undefined) {
1004
+ const validDrivers = [
1005
+ "subprocess",
1006
+ "api",
1007
+ "openai",
1008
+ "grok",
1009
+ "gemini",
1010
+ "gemini-api",
1011
+ "local",
1012
+ "none",
1013
+ ];
1014
+ if (!validDrivers.includes(driverRaw)) {
1015
+ res.writeHead(400, { "Content-Type": "application/json" });
1016
+ res.end(JSON.stringify({
1017
+ error: `driver must be one of: ${validDrivers.join(", ")}`,
1018
+ }));
1019
+ return;
1020
+ }
1021
+ const driver = driverRaw;
1022
+ cfg.driver = driver;
1023
+ try {
1024
+ saveBridgeConfigDriver(driver, this.bridgeConfigPath);
1025
+ }
1026
+ catch (writeErr) {
1027
+ this.logger.error(`[/config/patchwork] saveBridgeConfigDriver failed: ${writeErr instanceof Error ? (writeErr.stack ?? writeErr.message) : String(writeErr)}`);
1028
+ res.writeHead(500, { "Content-Type": "application/json" });
1029
+ res.end(JSON.stringify({
1030
+ error: "Failed to write bridge driver config",
1031
+ }));
1032
+ return;
1033
+ }
1034
+ }
1035
+ if (body.model !== undefined) {
1036
+ const validModels = [
1037
+ "claude",
1038
+ "openai",
1039
+ "gemini",
1040
+ "grok",
1041
+ "local",
1042
+ ];
1043
+ if (!validModels.includes(body.model)) {
1044
+ res.writeHead(400, { "Content-Type": "application/json" });
1045
+ res.end(JSON.stringify({
1046
+ error: `model must be one of: ${validModels.join(", ")}`,
1047
+ }));
1048
+ return;
1049
+ }
1050
+ cfg.model = body.model;
1051
+ if (body.model === "local") {
1052
+ if (body.localEndpoint !== undefined)
1053
+ cfg.localEndpoint = body.localEndpoint.trim() || undefined;
1054
+ if (body.localModel !== undefined)
1055
+ cfg.localModel = body.localModel.trim() || undefined;
1056
+ }
1057
+ }
1058
+ if (body.apiKey) {
1059
+ const { provider, key } = body.apiKey;
1060
+ const validProviders = ["anthropic", "openai", "google", "xai"];
1061
+ if (!validProviders.includes(provider) ||
1062
+ typeof key !== "string") {
1063
+ res.writeHead(400, { "Content-Type": "application/json" });
1064
+ res.end(JSON.stringify({ error: "Invalid apiKey provider or key" }));
1065
+ return;
1066
+ }
1067
+ // Provider keys go to the secure store (Keychain/DPAPI/Secret
1068
+ // Service / AES-256-GCM file fallback) — never persisted to
1069
+ // ~/.patchwork/config.json. Empty string clears.
1070
+ try {
1071
+ saveApiKeyToSecureStore(provider, key);
1072
+ }
1073
+ catch (writeErr) {
1074
+ this.logger.error(`[/config/patchwork] saveApiKeyToSecureStore failed: ${writeErr instanceof Error ? (writeErr.stack ?? writeErr.message) : String(writeErr)}`);
1075
+ res.writeHead(500, { "Content-Type": "application/json" });
1076
+ res.end(JSON.stringify({
1077
+ error: "Failed to write provider API key",
1078
+ }));
1079
+ return;
1080
+ }
1081
+ }
1082
+ try {
1083
+ savePatchworkConfig(cfg, configPath);
1084
+ }
1085
+ catch (writeErr) {
1086
+ this.logger.error(`[/config/patchwork] savePatchworkConfig failed: ${writeErr instanceof Error ? (writeErr.stack ?? writeErr.message) : String(writeErr)}`);
1087
+ res.writeHead(500, { "Content-Type": "application/json" });
1088
+ res.end(JSON.stringify({
1089
+ error: "Failed to write patchwork config",
1090
+ }));
1091
+ return;
1092
+ }
1093
+ if (hasWebhookUpdate) {
1094
+ this.approvalWebhookUrl = raw || undefined;
1095
+ }
1096
+ if (body.pushServiceUrl !== undefined) {
1097
+ const pushUrl = body.pushServiceUrl.trim();
1098
+ if (pushUrl && !pushUrl.startsWith("https://")) {
1099
+ res.writeHead(400, { "Content-Type": "application/json" });
1100
+ res.end(JSON.stringify({ error: "pushServiceUrl must be HTTPS" }));
1101
+ return;
1102
+ }
1103
+ this.pushServiceUrl = pushUrl || undefined;
1104
+ }
1105
+ if (body.pushServiceToken !== undefined) {
1106
+ this.pushServiceToken = body.pushServiceToken.trim() || undefined;
1107
+ }
1108
+ if (body.pushServiceBaseUrl !== undefined) {
1109
+ this.pushServiceBaseUrl =
1110
+ body.pushServiceBaseUrl.trim() || undefined;
1111
+ }
1112
+ const restartRequired = driverRaw !== undefined ||
1113
+ body.apiKey !== undefined ||
1114
+ body.model !== undefined;
1320
1115
  res.writeHead(200, { "Content-Type": "application/json" });
1321
- res.end(JSON.stringify({ ok: true }));
1116
+ res.end(JSON.stringify({ ok: true, restartRequired }));
1322
1117
  }
1323
- catch (err) {
1324
- res.writeHead(400, { "Content-Type": "application/json" });
1325
- res.end(JSON.stringify({
1326
- error: err instanceof Error ? err.message : String(err),
1327
- }));
1328
- }
1329
- });
1118
+ }
1119
+ catch (err) {
1120
+ this.logger.error(`[/config/patchwork] error: ${err instanceof Error ? (err.stack ?? err.message) : String(err)}`);
1121
+ res.writeHead(400, { "Content-Type": "application/json" });
1122
+ res.end(JSON.stringify({ error: "Invalid request body" }));
1123
+ }
1330
1124
  return;
1331
1125
  }
1332
1126
  // CC hook notify endpoint — lightweight alternative to full MCP session for hook wiring.
1333
1127
  if (parsedUrl.pathname === "/notify" && req.method === "POST") {
1334
- const chunks = [];
1335
- req.on("data", (c) => chunks.push(c));
1336
- req.on("end", () => {
1337
- try {
1338
- const body = Buffer.concat(chunks).toString("utf-8");
1339
- const parsed = JSON.parse(body);
1340
- const event = parsed.event ?? "";
1341
- const args = parsed.args ?? {};
1342
- if (!this.notifyFn) {
1343
- res.writeHead(503, { "Content-Type": "application/json" });
1344
- res.end(JSON.stringify({
1345
- ok: false,
1346
- error: "Automation not enabled",
1347
- }));
1348
- return;
1349
- }
1350
- const result = this.notifyFn(event, args);
1351
- res.writeHead(result.ok ? 200 : 400, {
1352
- "Content-Type": "application/json",
1353
- });
1354
- res.end(JSON.stringify(result));
1355
- }
1356
- catch {
1357
- res.writeHead(400, { "Content-Type": "application/json" });
1358
- res.end(JSON.stringify({ ok: false, error: "Invalid JSON body" }));
1128
+ // 8 KB — notify payloads carry an event name + small arg map
1129
+ // (taskId, prompt, tool name). 8 KB fits everything we send today
1130
+ // with headroom.
1131
+ const NOTIFY_BODY_CAP = 8 * 1024;
1132
+ const parsed = await readJsonBody(req, NOTIFY_BODY_CAP);
1133
+ if (!parsed.ok) {
1134
+ if (parsed.code === "too_large") {
1135
+ respond413(res, NOTIFY_BODY_CAP);
1136
+ return;
1359
1137
  }
1138
+ res.writeHead(400, { "Content-Type": "application/json" });
1139
+ res.end(JSON.stringify({ ok: false, error: "Invalid JSON body" }));
1140
+ return;
1141
+ }
1142
+ const event = parsed.value?.event ?? "";
1143
+ const args = parsed.value?.args ?? {};
1144
+ if (!this.notifyFn) {
1145
+ res.writeHead(503, { "Content-Type": "application/json" });
1146
+ res.end(JSON.stringify({
1147
+ ok: false,
1148
+ error: "Automation not enabled",
1149
+ }));
1150
+ return;
1151
+ }
1152
+ const result = this.notifyFn(event, args);
1153
+ res.writeHead(result.ok ? 200 : 400, {
1154
+ "Content-Type": "application/json",
1360
1155
  });
1156
+ res.end(JSON.stringify(result));
1361
1157
  return;
1362
1158
  }
1363
1159
  // Single-approval detail lookup for the dashboard detail page.
@@ -1375,100 +1171,113 @@ export class Server extends EventEmitter {
1375
1171
  res.end(JSON.stringify(data));
1376
1172
  }
1377
1173
  catch (err) {
1378
- res.writeHead(500, { "Content-Type": "application/json" });
1379
- res.end(JSON.stringify({
1380
- error: err instanceof Error ? err.message : String(err),
1381
- }));
1174
+ respond500(res, err);
1382
1175
  }
1383
1176
  return;
1384
1177
  }
1178
+ // SSE stream for live approval queue updates.
1179
+ if (parsedUrl.pathname === "/approvals/stream" && req.method === "GET") {
1180
+ handleApprovalsStream(res, { queue: getApprovalQueue() }, parsedUrl.searchParams.get("session"));
1181
+ return;
1182
+ }
1385
1183
  // Patchwork approval surface — PreToolUse hook + dashboard approve/reject.
1386
1184
  // Bearer auth already checked above.
1387
1185
  if (parsedUrl.pathname === "/approvals" ||
1388
1186
  parsedUrl.pathname === "/cc-permissions" ||
1389
1187
  /^\/(approve|reject)\/[A-Za-z0-9-]+$/.test(parsedUrl.pathname)) {
1390
- const chunks = [];
1391
- req.on("data", (c) => chunks.push(c));
1392
- req.on("end", async () => {
1393
- let parsedBody;
1394
- if (chunks.length > 0) {
1395
- try {
1396
- parsedBody = JSON.parse(Buffer.concat(chunks).toString("utf-8"));
1397
- }
1398
- catch {
1399
- res.writeHead(400, { "Content-Type": "application/json" });
1400
- res.end(JSON.stringify({ error: "invalid JSON body" }));
1401
- return;
1402
- }
1403
- }
1404
- try {
1405
- const result = await routeApprovalRequest({
1406
- method: req.method ?? "GET",
1407
- path: parsedUrl.pathname,
1408
- body: parsedBody,
1409
- query: parsedUrl.searchParams,
1410
- }, {
1411
- queue: getApprovalQueue(),
1412
- workspace: process.cwd(),
1413
- managedSettingsPath: this.managedSettingsPath,
1414
- onDecision: this.onApprovalDecision,
1415
- webhookUrl: this.approvalWebhookUrl,
1416
- approvalGate: this.approvalGate,
1417
- });
1418
- res.writeHead(result.status, {
1419
- "Content-Type": "application/json",
1420
- });
1421
- res.end(JSON.stringify(result.body));
1422
- }
1423
- catch (err) {
1424
- res.writeHead(500, { "Content-Type": "application/json" });
1425
- res.end(JSON.stringify({
1426
- error: err instanceof Error ? err.message : String(err),
1427
- }));
1188
+ // 32 KB — approvals carry decision + reason + optional permission
1189
+ // rule patches; 32 KB matches RECIPE_ROUTE_BODY_CAPS.run. Critical
1190
+ // here: /approve/:id and /reject/:id are reachable via the
1191
+ // x-approval-token phone-path bypass at line 609-612 — without a
1192
+ // cap an unbounded body read happens *before* token validation.
1193
+ const APPROVALS_BODY_CAP = 32 * 1024;
1194
+ const parsed = await readJsonBody(req, APPROVALS_BODY_CAP);
1195
+ if (!parsed.ok) {
1196
+ if (parsed.code === "too_large") {
1197
+ respond413(res, APPROVALS_BODY_CAP);
1198
+ return;
1428
1199
  }
1429
- });
1200
+ res.writeHead(400, { "Content-Type": "application/json" });
1201
+ res.end(JSON.stringify({ error: "invalid JSON body" }));
1202
+ return;
1203
+ }
1204
+ const parsedBody = parsed.value;
1205
+ try {
1206
+ const result = await routeApprovalRequest({
1207
+ method: req.method ?? "GET",
1208
+ path: parsedUrl.pathname,
1209
+ body: parsedBody,
1210
+ query: parsedUrl.searchParams,
1211
+ approvalToken: req.headers["x-approval-token"],
1212
+ }, {
1213
+ queue: getApprovalQueue(),
1214
+ workspace: process.cwd(),
1215
+ managedSettingsPath: this.managedSettingsPath,
1216
+ onDecision: this.onApprovalDecision,
1217
+ webhookUrl: this.approvalWebhookUrl,
1218
+ approvalGate: this.approvalGate,
1219
+ pushServiceUrl: this.pushServiceUrl,
1220
+ pushServiceToken: this.pushServiceToken,
1221
+ pushServiceBaseUrl: this.pushServiceBaseUrl,
1222
+ activityLog: this.activityLog,
1223
+ // RecipeRunLog satisfies RecipeRunQuerier structurally
1224
+ // — the cast bridges TS contravariance: RecipeRunQuerier's
1225
+ // narrow query interface (`status?: string`) is deliberately
1226
+ // loose so tests can mock it; RecipeRunLog's stricter
1227
+ // RunStatus union is a strict subset and fails the param
1228
+ // contravariance check despite being safe at runtime.
1229
+ recipeRunLog: this.recipeRunLog,
1230
+ enableTimeOfDayAnomaly: this.enableTimeOfDayAnomaly,
1231
+ });
1232
+ res.writeHead(result.status, {
1233
+ "Content-Type": "application/json",
1234
+ });
1235
+ res.end(JSON.stringify(result.body));
1236
+ }
1237
+ catch (err) {
1238
+ respond500(res, err);
1239
+ }
1430
1240
  return;
1431
1241
  }
1432
1242
  // Quick-task launch endpoint — mirrors /notify pattern. Bearer auth already checked above.
1433
1243
  if (parsedUrl.pathname === "/launch-quick-task" &&
1434
1244
  req.method === "POST") {
1435
- const chunks = [];
1436
- req.on("data", (c) => chunks.push(c));
1437
- req.on("end", () => {
1438
- void (async () => {
1439
- try {
1440
- const body = Buffer.concat(chunks).toString("utf-8");
1441
- const parsed = JSON.parse(body);
1442
- const presetId = parsed.presetId;
1443
- const source = parsed.source ?? "cli";
1444
- if (typeof presetId !== "string" || !presetId) {
1445
- res.writeHead(400, { "Content-Type": "application/json" });
1446
- res.end(JSON.stringify({
1447
- ok: false,
1448
- error: "presetId required",
1449
- }));
1450
- return;
1451
- }
1452
- if (!this.launchQuickTaskFn) {
1453
- res.writeHead(503, { "Content-Type": "application/json" });
1454
- res.end(JSON.stringify({
1455
- ok: false,
1456
- error: "Quick tasks unavailable — requires --claude-driver subprocess",
1457
- }));
1458
- return;
1459
- }
1460
- const result = await this.launchQuickTaskFn(presetId, source);
1461
- res.writeHead(result.ok ? 200 : 429, {
1462
- "Content-Type": "application/json",
1463
- });
1464
- res.end(JSON.stringify(result));
1465
- }
1466
- catch {
1467
- res.writeHead(400, { "Content-Type": "application/json" });
1468
- res.end(JSON.stringify({ ok: false, error: "Invalid JSON body" }));
1469
- }
1470
- })();
1245
+ // 4 KB — body is `{ presetId?: string; source?: string }`, two
1246
+ // short strings. 4 KB is plenty.
1247
+ const QUICK_TASK_BODY_CAP = 4 * 1024;
1248
+ const parsed = await readJsonBody(req, QUICK_TASK_BODY_CAP);
1249
+ if (!parsed.ok) {
1250
+ if (parsed.code === "too_large") {
1251
+ respond413(res, QUICK_TASK_BODY_CAP);
1252
+ return;
1253
+ }
1254
+ res.writeHead(400, { "Content-Type": "application/json" });
1255
+ res.end(JSON.stringify({ ok: false, error: "Invalid JSON body" }));
1256
+ return;
1257
+ }
1258
+ const presetId = parsed.value?.presetId;
1259
+ const source = parsed.value?.source ?? "cli";
1260
+ if (typeof presetId !== "string" || !presetId) {
1261
+ res.writeHead(400, { "Content-Type": "application/json" });
1262
+ res.end(JSON.stringify({
1263
+ ok: false,
1264
+ error: "presetId required",
1265
+ }));
1266
+ return;
1267
+ }
1268
+ if (!this.launchQuickTaskFn) {
1269
+ res.writeHead(503, { "Content-Type": "application/json" });
1270
+ res.end(JSON.stringify({
1271
+ ok: false,
1272
+ error: "Quick tasks unavailable — requires --claude-driver subprocess",
1273
+ }));
1274
+ return;
1275
+ }
1276
+ const result = await this.launchQuickTaskFn(presetId, source);
1277
+ res.writeHead(result.ok ? 200 : 429, {
1278
+ "Content-Type": "application/json",
1471
1279
  });
1280
+ res.end(JSON.stringify(result));
1472
1281
  return;
1473
1282
  }
1474
1283
  // MCP Streamable HTTP transport — POST/GET/DELETE /mcp.
@@ -1480,11 +1289,9 @@ export class Server extends EventEmitter {
1480
1289
  req.method === "GET" ||
1481
1290
  req.method === "DELETE") {
1482
1291
  this.httpMcpHandler(req, res).catch((err) => {
1483
- this.logger.error(`HTTP MCP handler error: ${err instanceof Error ? err.message : String(err)}`);
1484
- if (!res.headersSent) {
1485
- res.writeHead(500, { "Content-Type": "application/json" });
1486
- res.end(JSON.stringify({ error: String(err) }));
1487
- }
1292
+ // respond500 logs the underlying error detail server-side; no
1293
+ // need to also funnel it through this.logger.
1294
+ respond500(res, err, "/mcp HTTP handler");
1488
1295
  });
1489
1296
  return;
1490
1297
  }