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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (581) hide show
  1. package/README.bridge.md +6 -0
  2. package/README.md +315 -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/approvalHttp.d.ts +49 -2
  14. package/dist/approvalHttp.js +217 -21
  15. package/dist/approvalHttp.js.map +1 -1
  16. package/dist/approvalInsights.d.ts +49 -0
  17. package/dist/approvalInsights.js +97 -0
  18. package/dist/approvalInsights.js.map +1 -0
  19. package/dist/approvalQueue.d.ts +27 -1
  20. package/dist/approvalQueue.js +123 -3
  21. package/dist/approvalQueue.js.map +1 -1
  22. package/dist/approvalSignals.d.ts +124 -0
  23. package/dist/approvalSignals.js +512 -0
  24. package/dist/approvalSignals.js.map +1 -0
  25. package/dist/automation.d.ts +57 -0
  26. package/dist/automation.js +156 -59
  27. package/dist/automation.js.map +1 -1
  28. package/dist/automationSuggestions.d.ts +79 -0
  29. package/dist/automationSuggestions.js +150 -0
  30. package/dist/automationSuggestions.js.map +1 -0
  31. package/dist/bridge.d.ts +3 -0
  32. package/dist/bridge.js +174 -143
  33. package/dist/bridge.js.map +1 -1
  34. package/dist/bridgeToken.js +57 -19
  35. package/dist/bridgeToken.js.map +1 -1
  36. package/dist/ccPermissions.d.ts +15 -0
  37. package/dist/ccPermissions.js +21 -4
  38. package/dist/ccPermissions.js.map +1 -1
  39. package/dist/claudeDriver.js +74 -16
  40. package/dist/claudeDriver.js.map +1 -1
  41. package/dist/claudeOrchestrator.d.ts +1 -1
  42. package/dist/claudeOrchestrator.js +14 -8
  43. package/dist/claudeOrchestrator.js.map +1 -1
  44. package/dist/commands/dashboard.js +1 -1
  45. package/dist/commands/dashboard.js.map +1 -1
  46. package/dist/commands/launchd.d.ts +2 -0
  47. package/dist/commands/launchd.js +94 -0
  48. package/dist/commands/launchd.js.map +1 -0
  49. package/dist/commands/patchworkInit.d.ts +8 -0
  50. package/dist/commands/patchworkInit.js +77 -11
  51. package/dist/commands/patchworkInit.js.map +1 -1
  52. package/dist/commands/recipe.d.ts +289 -0
  53. package/dist/commands/recipe.js +1359 -0
  54. package/dist/commands/recipe.js.map +1 -0
  55. package/dist/commands/recipeInstall.d.ts +150 -0
  56. package/dist/commands/recipeInstall.js +647 -0
  57. package/dist/commands/recipeInstall.js.map +1 -0
  58. package/dist/commands/tracesExport.d.ts +83 -0
  59. package/dist/commands/tracesExport.js +269 -0
  60. package/dist/commands/tracesExport.js.map +1 -0
  61. package/dist/commands/tracesImport.d.ts +56 -0
  62. package/dist/commands/tracesImport.js +161 -0
  63. package/dist/commands/tracesImport.js.map +1 -0
  64. package/dist/config.d.ts +22 -1
  65. package/dist/config.js +108 -9
  66. package/dist/config.js.map +1 -1
  67. package/dist/connectorRoutes.d.ts +43 -0
  68. package/dist/connectorRoutes.js +1609 -0
  69. package/dist/connectorRoutes.js.map +1 -0
  70. package/dist/connectors/asana.d.ts +198 -0
  71. package/dist/connectors/asana.js +679 -0
  72. package/dist/connectors/asana.js.map +1 -0
  73. package/dist/connectors/baseConnector.d.ts +153 -0
  74. package/dist/connectors/baseConnector.js +336 -0
  75. package/dist/connectors/baseConnector.js.map +1 -0
  76. package/dist/connectors/confluence.d.ts +111 -0
  77. package/dist/connectors/confluence.js +406 -0
  78. package/dist/connectors/confluence.js.map +1 -0
  79. package/dist/connectors/datadog.d.ts +116 -0
  80. package/dist/connectors/datadog.js +385 -0
  81. package/dist/connectors/datadog.js.map +1 -0
  82. package/dist/connectors/discord.d.ts +150 -0
  83. package/dist/connectors/discord.js +543 -0
  84. package/dist/connectors/discord.js.map +1 -0
  85. package/dist/connectors/fixtureLibrary.d.ts +21 -0
  86. package/dist/connectors/fixtureLibrary.js +70 -0
  87. package/dist/connectors/fixtureLibrary.js.map +1 -0
  88. package/dist/connectors/fixtureRecorder.d.ts +1 -0
  89. package/dist/connectors/fixtureRecorder.js +35 -0
  90. package/dist/connectors/fixtureRecorder.js.map +1 -0
  91. package/dist/connectors/github.js +17 -18
  92. package/dist/connectors/github.js.map +1 -1
  93. package/dist/connectors/gitlab.d.ts +180 -0
  94. package/dist/connectors/gitlab.js +582 -0
  95. package/dist/connectors/gitlab.js.map +1 -0
  96. package/dist/connectors/gmail.d.ts +4 -1
  97. package/dist/connectors/gmail.js +149 -27
  98. package/dist/connectors/gmail.js.map +1 -1
  99. package/dist/connectors/googleCalendar.d.ts +4 -1
  100. package/dist/connectors/googleCalendar.js +88 -25
  101. package/dist/connectors/googleCalendar.js.map +1 -1
  102. package/dist/connectors/googleDrive.d.ts +34 -0
  103. package/dist/connectors/googleDrive.js +321 -0
  104. package/dist/connectors/googleDrive.js.map +1 -0
  105. package/dist/connectors/htmlEscape.d.ts +5 -0
  106. package/dist/connectors/htmlEscape.js +13 -0
  107. package/dist/connectors/htmlEscape.js.map +1 -0
  108. package/dist/connectors/hubspot.d.ts +112 -0
  109. package/dist/connectors/hubspot.js +408 -0
  110. package/dist/connectors/hubspot.js.map +1 -0
  111. package/dist/connectors/intercom.d.ts +102 -0
  112. package/dist/connectors/intercom.js +402 -0
  113. package/dist/connectors/intercom.js.map +1 -0
  114. package/dist/connectors/jira.d.ts +98 -0
  115. package/dist/connectors/jira.js +379 -0
  116. package/dist/connectors/jira.js.map +1 -0
  117. package/dist/connectors/linear.js +30 -19
  118. package/dist/connectors/linear.js.map +1 -1
  119. package/dist/connectors/mcpOAuth.d.ts +3 -0
  120. package/dist/connectors/mcpOAuth.js +64 -10
  121. package/dist/connectors/mcpOAuth.js.map +1 -1
  122. package/dist/connectors/mockConnector.d.ts +28 -0
  123. package/dist/connectors/mockConnector.js +81 -0
  124. package/dist/connectors/mockConnector.js.map +1 -0
  125. package/dist/connectors/notion.d.ts +143 -0
  126. package/dist/connectors/notion.js +424 -0
  127. package/dist/connectors/notion.js.map +1 -0
  128. package/dist/connectors/oauthStateStore.d.ts +31 -0
  129. package/dist/connectors/oauthStateStore.js +52 -0
  130. package/dist/connectors/oauthStateStore.js.map +1 -0
  131. package/dist/connectors/pagerduty.d.ts +160 -0
  132. package/dist/connectors/pagerduty.js +464 -0
  133. package/dist/connectors/pagerduty.js.map +1 -0
  134. package/dist/connectors/sentry.js +5 -13
  135. package/dist/connectors/sentry.js.map +1 -1
  136. package/dist/connectors/slack.d.ts +16 -1
  137. package/dist/connectors/slack.js +155 -32
  138. package/dist/connectors/slack.js.map +1 -1
  139. package/dist/connectors/stripe.d.ts +116 -0
  140. package/dist/connectors/stripe.js +379 -0
  141. package/dist/connectors/stripe.js.map +1 -0
  142. package/dist/connectors/tokenStorage.d.ts +35 -0
  143. package/dist/connectors/tokenStorage.js +484 -0
  144. package/dist/connectors/tokenStorage.js.map +1 -0
  145. package/dist/connectors/zendesk.d.ts +104 -0
  146. package/dist/connectors/zendesk.js +442 -0
  147. package/dist/connectors/zendesk.js.map +1 -0
  148. package/dist/cors.d.ts +10 -0
  149. package/dist/cors.js +29 -0
  150. package/dist/cors.js.map +1 -0
  151. package/dist/decisionReplay.d.ts +72 -0
  152. package/dist/decisionReplay.js +92 -0
  153. package/dist/decisionReplay.js.map +1 -0
  154. package/dist/decisionTraceLog.d.ts +6 -0
  155. package/dist/decisionTraceLog.js +54 -2
  156. package/dist/decisionTraceLog.js.map +1 -1
  157. package/dist/drivers/gemini/index.d.ts +5 -1
  158. package/dist/drivers/gemini/index.js +39 -5
  159. package/dist/drivers/gemini/index.js.map +1 -1
  160. package/dist/drivers/index.d.ts +5 -0
  161. package/dist/drivers/index.js +1 -1
  162. package/dist/drivers/index.js.map +1 -1
  163. package/dist/featureFlags.d.ts +79 -0
  164. package/dist/featureFlags.js +208 -0
  165. package/dist/featureFlags.js.map +1 -0
  166. package/dist/fp/automationInterpreter.js +26 -21
  167. package/dist/fp/automationInterpreter.js.map +1 -1
  168. package/dist/fp/automationProgram.d.ts +1 -1
  169. package/dist/fp/automationProgram.js.map +1 -1
  170. package/dist/fp/automationState.js +4 -1
  171. package/dist/fp/automationState.js.map +1 -1
  172. package/dist/fp/policyParser.js +21 -1
  173. package/dist/fp/policyParser.js.map +1 -1
  174. package/dist/inboxRoutes.d.ts +22 -0
  175. package/dist/inboxRoutes.js +114 -0
  176. package/dist/inboxRoutes.js.map +1 -0
  177. package/dist/index.js +1400 -201
  178. package/dist/index.js.map +1 -1
  179. package/dist/installGuard.d.ts +25 -0
  180. package/dist/installGuard.js +48 -0
  181. package/dist/installGuard.js.map +1 -0
  182. package/dist/mcpRoutes.d.ts +37 -0
  183. package/dist/mcpRoutes.js +76 -0
  184. package/dist/mcpRoutes.js.map +1 -0
  185. package/dist/oauth.d.ts +7 -1
  186. package/dist/oauth.js +201 -39
  187. package/dist/oauth.js.map +1 -1
  188. package/dist/oauthRoutes.d.ts +32 -0
  189. package/dist/oauthRoutes.js +124 -0
  190. package/dist/oauthRoutes.js.map +1 -0
  191. package/dist/orchestrator/orchestratorBridge.js +2 -2
  192. package/dist/orchestrator/orchestratorBridge.js.map +1 -1
  193. package/dist/patchworkConfig.d.ts +16 -0
  194. package/dist/patchworkConfig.js +1 -1
  195. package/dist/patchworkConfig.js.map +1 -1
  196. package/dist/pluginLoader.d.ts +28 -0
  197. package/dist/pluginLoader.js +77 -11
  198. package/dist/pluginLoader.js.map +1 -1
  199. package/dist/pluginWatcher.js +8 -3
  200. package/dist/pluginWatcher.js.map +1 -1
  201. package/dist/preToolUseHook.d.ts +12 -0
  202. package/dist/preToolUseHook.js +23 -0
  203. package/dist/preToolUseHook.js.map +1 -1
  204. package/dist/recipeOrchestration.d.ts +121 -0
  205. package/dist/recipeOrchestration.js +955 -0
  206. package/dist/recipeOrchestration.js.map +1 -0
  207. package/dist/recipeRoutes.d.ts +180 -0
  208. package/dist/recipeRoutes.js +1345 -0
  209. package/dist/recipeRoutes.js.map +1 -0
  210. package/dist/recipes/RecipeOrchestrator.d.ts +40 -0
  211. package/dist/recipes/RecipeOrchestrator.js +51 -0
  212. package/dist/recipes/RecipeOrchestrator.js.map +1 -0
  213. package/dist/recipes/agentExecutor.d.ts +29 -0
  214. package/dist/recipes/agentExecutor.js +49 -0
  215. package/dist/recipes/agentExecutor.js.map +1 -0
  216. package/dist/recipes/chainedRunner.d.ts +191 -0
  217. package/dist/recipes/chainedRunner.js +759 -0
  218. package/dist/recipes/chainedRunner.js.map +1 -0
  219. package/dist/recipes/compiler.js +3 -3
  220. package/dist/recipes/compiler.js.map +1 -1
  221. package/dist/recipes/dependencyGraph.d.ts +39 -0
  222. package/dist/recipes/dependencyGraph.js +199 -0
  223. package/dist/recipes/dependencyGraph.js.map +1 -0
  224. package/dist/recipes/disabledMarkers.d.ts +48 -0
  225. package/dist/recipes/disabledMarkers.js +52 -0
  226. package/dist/recipes/disabledMarkers.js.map +1 -0
  227. package/dist/recipes/installer.js +3 -3
  228. package/dist/recipes/installer.js.map +1 -1
  229. package/dist/recipes/legacyRecipeCompat.d.ts +10 -0
  230. package/dist/recipes/legacyRecipeCompat.js +131 -0
  231. package/dist/recipes/legacyRecipeCompat.js.map +1 -0
  232. package/dist/recipes/manifest.d.ts +47 -0
  233. package/dist/recipes/manifest.js +156 -0
  234. package/dist/recipes/manifest.js.map +1 -0
  235. package/dist/recipes/migrationWarnings.d.ts +12 -0
  236. package/dist/recipes/migrationWarnings.js +44 -0
  237. package/dist/recipes/migrationWarnings.js.map +1 -0
  238. package/dist/recipes/migrations/index.d.ts +24 -0
  239. package/dist/recipes/migrations/index.js +55 -0
  240. package/dist/recipes/migrations/index.js.map +1 -0
  241. package/dist/recipes/migrations/types.d.ts +28 -0
  242. package/dist/recipes/migrations/types.js +2 -0
  243. package/dist/recipes/migrations/types.js.map +1 -0
  244. package/dist/recipes/migrations/v1.d.ts +11 -0
  245. package/dist/recipes/migrations/v1.js +18 -0
  246. package/dist/recipes/migrations/v1.js.map +1 -0
  247. package/dist/recipes/names.d.ts +40 -0
  248. package/dist/recipes/names.js +66 -0
  249. package/dist/recipes/names.js.map +1 -0
  250. package/dist/recipes/nestedRecipeStep.d.ts +58 -0
  251. package/dist/recipes/nestedRecipeStep.js +95 -0
  252. package/dist/recipes/nestedRecipeStep.js.map +1 -0
  253. package/dist/recipes/outputRegistry.d.ts +28 -0
  254. package/dist/recipes/outputRegistry.js +52 -0
  255. package/dist/recipes/outputRegistry.js.map +1 -0
  256. package/dist/recipes/parser.js +4 -1
  257. package/dist/recipes/parser.js.map +1 -1
  258. package/dist/recipes/replayRun.d.ts +62 -0
  259. package/dist/recipes/replayRun.js +97 -0
  260. package/dist/recipes/replayRun.js.map +1 -0
  261. package/dist/recipes/resolveRecipePath.d.ts +69 -0
  262. package/dist/recipes/resolveRecipePath.js +202 -0
  263. package/dist/recipes/resolveRecipePath.js.map +1 -0
  264. package/dist/recipes/scheduler.d.ts +23 -7
  265. package/dist/recipes/scheduler.js +225 -45
  266. package/dist/recipes/scheduler.js.map +1 -1
  267. package/dist/recipes/schema.d.ts +17 -2
  268. package/dist/recipes/schemaGenerator.d.ts +28 -0
  269. package/dist/recipes/schemaGenerator.js +565 -0
  270. package/dist/recipes/schemaGenerator.js.map +1 -0
  271. package/dist/recipes/stepObservation.d.ts +44 -0
  272. package/dist/recipes/stepObservation.js +232 -0
  273. package/dist/recipes/stepObservation.js.map +1 -0
  274. package/dist/recipes/templateEngine.d.ts +62 -0
  275. package/dist/recipes/templateEngine.js +201 -0
  276. package/dist/recipes/templateEngine.js.map +1 -0
  277. package/dist/recipes/toolRegistry.d.ts +186 -0
  278. package/dist/recipes/toolRegistry.js +309 -0
  279. package/dist/recipes/toolRegistry.js.map +1 -0
  280. package/dist/recipes/tools/asana.d.ts +16 -0
  281. package/dist/recipes/tools/asana.js +524 -0
  282. package/dist/recipes/tools/asana.js.map +1 -0
  283. package/dist/recipes/tools/calendar.d.ts +6 -0
  284. package/dist/recipes/tools/calendar.js +61 -0
  285. package/dist/recipes/tools/calendar.js.map +1 -0
  286. package/dist/recipes/tools/confluence.d.ts +6 -0
  287. package/dist/recipes/tools/confluence.js +254 -0
  288. package/dist/recipes/tools/confluence.js.map +1 -0
  289. package/dist/recipes/tools/datadog.d.ts +6 -0
  290. package/dist/recipes/tools/datadog.js +239 -0
  291. package/dist/recipes/tools/datadog.js.map +1 -0
  292. package/dist/recipes/tools/diagnostics.d.ts +6 -0
  293. package/dist/recipes/tools/diagnostics.js +36 -0
  294. package/dist/recipes/tools/diagnostics.js.map +1 -0
  295. package/dist/recipes/tools/discord.d.ts +18 -0
  296. package/dist/recipes/tools/discord.js +254 -0
  297. package/dist/recipes/tools/discord.js.map +1 -0
  298. package/dist/recipes/tools/file.d.ts +12 -0
  299. package/dist/recipes/tools/file.js +174 -0
  300. package/dist/recipes/tools/file.js.map +1 -0
  301. package/dist/recipes/tools/git.d.ts +6 -0
  302. package/dist/recipes/tools/git.js +63 -0
  303. package/dist/recipes/tools/git.js.map +1 -0
  304. package/dist/recipes/tools/github.d.ts +6 -0
  305. package/dist/recipes/tools/github.js +116 -0
  306. package/dist/recipes/tools/github.js.map +1 -0
  307. package/dist/recipes/tools/gitlab.d.ts +11 -0
  308. package/dist/recipes/tools/gitlab.js +285 -0
  309. package/dist/recipes/tools/gitlab.js.map +1 -0
  310. package/dist/recipes/tools/gmail.d.ts +6 -0
  311. package/dist/recipes/tools/gmail.js +434 -0
  312. package/dist/recipes/tools/gmail.js.map +1 -0
  313. package/dist/recipes/tools/googleDrive.d.ts +1 -0
  314. package/dist/recipes/tools/googleDrive.js +55 -0
  315. package/dist/recipes/tools/googleDrive.js.map +1 -0
  316. package/dist/recipes/tools/hubspot.d.ts +6 -0
  317. package/dist/recipes/tools/hubspot.js +232 -0
  318. package/dist/recipes/tools/hubspot.js.map +1 -0
  319. package/dist/recipes/tools/index.d.ts +30 -0
  320. package/dist/recipes/tools/index.js +33 -0
  321. package/dist/recipes/tools/index.js.map +1 -0
  322. package/dist/recipes/tools/intercom.d.ts +6 -0
  323. package/dist/recipes/tools/intercom.js +226 -0
  324. package/dist/recipes/tools/intercom.js.map +1 -0
  325. package/dist/recipes/tools/jira.d.ts +14 -0
  326. package/dist/recipes/tools/jira.js +369 -0
  327. package/dist/recipes/tools/jira.js.map +1 -0
  328. package/dist/recipes/tools/linear.d.ts +7 -0
  329. package/dist/recipes/tools/linear.js +307 -0
  330. package/dist/recipes/tools/linear.js.map +1 -0
  331. package/dist/recipes/tools/meetingNotes.d.ts +21 -0
  332. package/dist/recipes/tools/meetingNotes.js +701 -0
  333. package/dist/recipes/tools/meetingNotes.js.map +1 -0
  334. package/dist/recipes/tools/notion.d.ts +6 -0
  335. package/dist/recipes/tools/notion.js +278 -0
  336. package/dist/recipes/tools/notion.js.map +1 -0
  337. package/dist/recipes/tools/pagerduty.d.ts +15 -0
  338. package/dist/recipes/tools/pagerduty.js +451 -0
  339. package/dist/recipes/tools/pagerduty.js.map +1 -0
  340. package/dist/recipes/tools/sentry.d.ts +12 -0
  341. package/dist/recipes/tools/sentry.js +73 -0
  342. package/dist/recipes/tools/sentry.js.map +1 -0
  343. package/dist/recipes/tools/slack.d.ts +6 -0
  344. package/dist/recipes/tools/slack.js +82 -0
  345. package/dist/recipes/tools/slack.js.map +1 -0
  346. package/dist/recipes/tools/stripe.d.ts +6 -0
  347. package/dist/recipes/tools/stripe.js +265 -0
  348. package/dist/recipes/tools/stripe.js.map +1 -0
  349. package/dist/recipes/tools/zendesk.d.ts +6 -0
  350. package/dist/recipes/tools/zendesk.js +245 -0
  351. package/dist/recipes/tools/zendesk.js.map +1 -0
  352. package/dist/recipes/validation.d.ts +13 -0
  353. package/dist/recipes/validation.js +617 -0
  354. package/dist/recipes/validation.js.map +1 -0
  355. package/dist/recipes/yamlRunner.d.ts +116 -1
  356. package/dist/recipes/yamlRunner.js +1000 -401
  357. package/dist/recipes/yamlRunner.js.map +1 -1
  358. package/dist/recipesHttp.d.ts +137 -6
  359. package/dist/recipesHttp.js +941 -29
  360. package/dist/recipesHttp.js.map +1 -1
  361. package/dist/riskTier.js +7 -1
  362. package/dist/riskTier.js.map +1 -1
  363. package/dist/runLog.d.ts +100 -1
  364. package/dist/runLog.js +258 -5
  365. package/dist/runLog.js.map +1 -1
  366. package/dist/schemas/dry-run-plan.v1.json +139 -0
  367. package/dist/schemas/recipe.v1.json +684 -0
  368. package/dist/server.d.ts +121 -8
  369. package/dist/server.js +538 -735
  370. package/dist/server.js.map +1 -1
  371. package/dist/ssrfGuard.d.ts +54 -0
  372. package/dist/ssrfGuard.js +122 -0
  373. package/dist/ssrfGuard.js.map +1 -0
  374. package/dist/streamableHttp.d.ts +39 -1
  375. package/dist/streamableHttp.js +128 -17
  376. package/dist/streamableHttp.js.map +1 -1
  377. package/dist/tokenUsageTracker.d.ts +33 -0
  378. package/dist/tokenUsageTracker.js +146 -0
  379. package/dist/tokenUsageTracker.js.map +1 -0
  380. package/dist/tools/activityLog.d.ts +2 -0
  381. package/dist/tools/addLinearComment.d.ts +1 -0
  382. package/dist/tools/addLinearComment.js +4 -2
  383. package/dist/tools/addLinearComment.js.map +1 -1
  384. package/dist/tools/batchLsp.d.ts +3 -0
  385. package/dist/tools/bridgeDoctor.d.ts +1 -0
  386. package/dist/tools/bridgeDoctor.js +2 -2
  387. package/dist/tools/bridgeDoctor.js.map +1 -1
  388. package/dist/tools/bridgeStatus.d.ts +1 -0
  389. package/dist/tools/cancelClaudeTask.d.ts +2 -0
  390. package/dist/tools/cancelClaudeTask.js +1 -0
  391. package/dist/tools/cancelClaudeTask.js.map +1 -1
  392. package/dist/tools/checkDocumentDirty.d.ts +1 -0
  393. package/dist/tools/clipboard.d.ts +2 -0
  394. package/dist/tools/closeTabs.d.ts +2 -0
  395. package/dist/tools/codeLens.d.ts +1 -0
  396. package/dist/tools/contextBundle.d.ts +1 -0
  397. package/dist/tools/createIssueFromAIComment.d.ts +1 -0
  398. package/dist/tools/createLinearIssue.d.ts +1 -0
  399. package/dist/tools/ctxGetTaskContext.d.ts +1 -0
  400. package/dist/tools/ctxQueryTraces.d.ts +1 -0
  401. package/dist/tools/ctxSaveTrace.d.ts +1 -0
  402. package/dist/tools/debug.d.ts +4 -0
  403. package/dist/tools/decorations.d.ts +2 -0
  404. package/dist/tools/documentLinks.d.ts +1 -0
  405. package/dist/tools/editText.d.ts +1 -0
  406. package/dist/tools/enrichCommit.d.ts +1 -0
  407. package/dist/tools/enrichStackTrace.d.ts +1 -0
  408. package/dist/tools/explainDiagnostic.d.ts +1 -0
  409. package/dist/tools/explainSymbol.d.ts +1 -0
  410. package/dist/tools/fetchCalendarEvents.d.ts +1 -0
  411. package/dist/tools/fetchGithubIssue.d.ts +1 -0
  412. package/dist/tools/fetchGithubPR.d.ts +1 -0
  413. package/dist/tools/fetchLinearIssue.d.ts +1 -0
  414. package/dist/tools/fetchSentryIssue.d.ts +1 -0
  415. package/dist/tools/fetchSlackProfile.d.ts +1 -0
  416. package/dist/tools/fetchSlackProfile.js +4 -1
  417. package/dist/tools/fetchSlackProfile.js.map +1 -1
  418. package/dist/tools/fileOperations.d.ts +3 -0
  419. package/dist/tools/fileWatcher.d.ts +2 -0
  420. package/dist/tools/findFiles.d.ts +1 -0
  421. package/dist/tools/findRelatedTests.d.ts +1 -0
  422. package/dist/tools/fixAllLintErrors.d.ts +1 -0
  423. package/dist/tools/foldingRanges.d.ts +1 -0
  424. package/dist/tools/formatDocument.d.ts +1 -0
  425. package/dist/tools/generateTests.d.ts +1 -0
  426. package/dist/tools/getAIComments.d.ts +1 -0
  427. package/dist/tools/getAnalyticsReport.d.ts +1 -0
  428. package/dist/tools/getArchitectureContext.d.ts +1 -0
  429. package/dist/tools/getBufferContent.d.ts +1 -0
  430. package/dist/tools/getChangeImpact.d.ts +1 -0
  431. package/dist/tools/getClaudeTaskStatus.d.ts +2 -0
  432. package/dist/tools/getClaudeTaskStatus.js +1 -0
  433. package/dist/tools/getClaudeTaskStatus.js.map +1 -1
  434. package/dist/tools/getCodeCoverage.d.ts +1 -0
  435. package/dist/tools/getCommitsForIssue.d.ts +1 -0
  436. package/dist/tools/getConnectorStatus.d.ts +1 -0
  437. package/dist/tools/getCurrentSelection.d.ts +2 -0
  438. package/dist/tools/getDebugState.d.ts +1 -0
  439. package/dist/tools/getDependencyTree.d.ts +1 -0
  440. package/dist/tools/getDiagnostics.d.ts +1 -0
  441. package/dist/tools/getDiffFromHandoff.d.ts +1 -0
  442. package/dist/tools/getDocumentSymbols.d.ts +25 -0
  443. package/dist/tools/getDocumentSymbols.js +74 -8
  444. package/dist/tools/getDocumentSymbols.js.map +1 -1
  445. package/dist/tools/getFileTree.d.ts +1 -0
  446. package/dist/tools/getGitDiff.d.ts +1 -0
  447. package/dist/tools/getGitHotspots.d.ts +1 -0
  448. package/dist/tools/getGitLog.d.ts +1 -0
  449. package/dist/tools/getGitStatus.d.ts +1 -0
  450. package/dist/tools/getImportTree.d.ts +1 -0
  451. package/dist/tools/getImportedSignatures.d.ts +1 -0
  452. package/dist/tools/getOpenEditors.d.ts +1 -0
  453. package/dist/tools/getPRTemplate.d.ts +1 -0
  454. package/dist/tools/getProjectContext.d.ts +1 -0
  455. package/dist/tools/getProjectInfo.d.ts +1 -0
  456. package/dist/tools/getSecurityAdvisories.d.ts +1 -0
  457. package/dist/tools/getSecurityAdvisories.js +10 -1
  458. package/dist/tools/getSecurityAdvisories.js.map +1 -1
  459. package/dist/tools/getSessionUsage.d.ts +4 -0
  460. package/dist/tools/getSessionUsage.js +3 -0
  461. package/dist/tools/getSessionUsage.js.map +1 -1
  462. package/dist/tools/getSymbolHistory.d.ts +1 -0
  463. package/dist/tools/getToolCapabilities.d.ts +1 -0
  464. package/dist/tools/getTypeSignature.d.ts +1 -0
  465. package/dist/tools/getWorkspaceFolders.d.ts +1 -0
  466. package/dist/tools/getWorkspaceSettings.d.ts +1 -0
  467. package/dist/tools/gitHistory.d.ts +2 -0
  468. package/dist/tools/gitWrite.d.ts +11 -0
  469. package/dist/tools/github/actions.d.ts +2 -0
  470. package/dist/tools/github/actions.js +4 -2
  471. package/dist/tools/github/actions.js.map +1 -1
  472. package/dist/tools/github/composite.d.ts +342 -0
  473. package/dist/tools/github/composite.js +343 -0
  474. package/dist/tools/github/composite.js.map +1 -0
  475. package/dist/tools/github/index.d.ts +1 -0
  476. package/dist/tools/github/index.js +1 -0
  477. package/dist/tools/github/index.js.map +1 -1
  478. package/dist/tools/github/issues.d.ts +4 -0
  479. package/dist/tools/github/issues.js +8 -4
  480. package/dist/tools/github/issues.js.map +1 -1
  481. package/dist/tools/github/pr.d.ts +7 -0
  482. package/dist/tools/github/pr.js +50 -12
  483. package/dist/tools/github/pr.js.map +1 -1
  484. package/dist/tools/handoffNote.d.ts +4 -0
  485. package/dist/tools/handoffNote.js +2 -0
  486. package/dist/tools/handoffNote.js.map +1 -1
  487. package/dist/tools/hoverAtCursor.d.ts +1 -0
  488. package/dist/tools/httpClient.d.ts +2 -0
  489. package/dist/tools/index.d.ts +8 -0
  490. package/dist/tools/index.js +47 -8
  491. package/dist/tools/index.js.map +1 -1
  492. package/dist/tools/inlayHints.d.ts +1 -0
  493. package/dist/tools/launchQuickTask.d.ts +2 -0
  494. package/dist/tools/launchQuickTask.js +1 -0
  495. package/dist/tools/launchQuickTask.js.map +1 -1
  496. package/dist/tools/listClaudeTasks.d.ts +2 -0
  497. package/dist/tools/listClaudeTasks.js +1 -0
  498. package/dist/tools/listClaudeTasks.js.map +1 -1
  499. package/dist/tools/listTerminals.d.ts +1 -0
  500. package/dist/tools/lsp.d.ts +14 -0
  501. package/dist/tools/navigateToSymbolByName.d.ts +1 -0
  502. package/dist/tools/openDiff.d.ts +1 -0
  503. package/dist/tools/openFile.d.ts +1 -0
  504. package/dist/tools/openInBrowser.d.ts +1 -0
  505. package/dist/tools/organizeImports.d.ts +1 -0
  506. package/dist/tools/performanceReport.d.ts +1 -0
  507. package/dist/tools/planPersistence.d.ts +5 -0
  508. package/dist/tools/previewEdit.d.ts +1 -0
  509. package/dist/tools/refactorAnalyze.d.ts +1 -0
  510. package/dist/tools/refactorPreview.d.ts +2 -0
  511. package/dist/tools/refactorPreview.js +1 -0
  512. package/dist/tools/refactorPreview.js.map +1 -1
  513. package/dist/tools/replaceBlock.d.ts +1 -0
  514. package/dist/tools/resumeClaudeTask.d.ts +2 -0
  515. package/dist/tools/resumeClaudeTask.js +1 -0
  516. package/dist/tools/resumeClaudeTask.js.map +1 -1
  517. package/dist/tools/runClaudeTask.d.ts +2 -0
  518. package/dist/tools/runClaudeTask.js +1 -0
  519. package/dist/tools/runClaudeTask.js.map +1 -1
  520. package/dist/tools/runCommand.d.ts +1 -0
  521. package/dist/tools/runTests.d.ts +1 -0
  522. package/dist/tools/saveDocument.d.ts +1 -0
  523. package/dist/tools/screenshotAndAnnotate.d.ts +1 -0
  524. package/dist/tools/searchAndReplace.d.ts +1 -0
  525. package/dist/tools/searchTools.d.ts +1 -0
  526. package/dist/tools/searchTools.js +1 -1
  527. package/dist/tools/searchTools.js.map +1 -1
  528. package/dist/tools/searchWorkspace.d.ts +1 -0
  529. package/dist/tools/selectionRanges.d.ts +1 -0
  530. package/dist/tools/semanticTokens.d.ts +1 -0
  531. package/dist/tools/setActiveWorkspaceFolder.d.ts +1 -0
  532. package/dist/tools/signatureHelp.d.ts +1 -0
  533. package/dist/tools/slackListChannels.d.ts +1 -0
  534. package/dist/tools/slackListChannels.js.map +1 -1
  535. package/dist/tools/slackPostMessage.d.ts +1 -0
  536. package/dist/tools/slackPostMessage.js +11 -6
  537. package/dist/tools/slackPostMessage.js.map +1 -1
  538. package/dist/tools/terminal.d.ts +6 -0
  539. package/dist/tools/testTraceToSource.d.ts +1 -0
  540. package/dist/tools/testTraceToSource.js +2 -2
  541. package/dist/tools/testTraceToSource.js.map +1 -1
  542. package/dist/tools/transaction.d.ts +23 -0
  543. package/dist/tools/transaction.js +29 -0
  544. package/dist/tools/transaction.js.map +1 -1
  545. package/dist/tools/typeHierarchy.d.ts +1 -0
  546. package/dist/tools/updateLinearIssue.d.ts +1 -0
  547. package/dist/tools/updateLinearIssue.js +20 -6
  548. package/dist/tools/updateLinearIssue.js.map +1 -1
  549. package/dist/tools/utils.d.ts +2 -0
  550. package/dist/tools/utils.js.map +1 -1
  551. package/dist/tools/vscodeCommands.d.ts +2 -0
  552. package/dist/tools/vscodeTasks.d.ts +2 -0
  553. package/dist/tools/workspaceSettings.d.ts +1 -0
  554. package/dist/traceEncryption.d.ts +46 -0
  555. package/dist/traceEncryption.js +124 -0
  556. package/dist/traceEncryption.js.map +1 -0
  557. package/dist/transport.d.ts +46 -1
  558. package/dist/transport.js +173 -19
  559. package/dist/transport.js.map +1 -1
  560. package/package.json +30 -8
  561. package/scripts/mcp-stdio-shim.cjs +19 -3
  562. package/scripts/start-all.sh +30 -1
  563. package/templates/automation-policies/recipe-authoring.json +25 -0
  564. package/templates/automation-policy.example.json +6 -0
  565. package/templates/co.patchwork-os.bridge.plist +34 -0
  566. package/templates/policies/README.md +72 -0
  567. package/templates/policies/conservative.json +14 -0
  568. package/templates/policies/developer.json +14 -0
  569. package/templates/policies/headless-ci.json +24 -0
  570. package/templates/policies/personal-assistant.json +15 -0
  571. package/templates/policies/regulated-industry.json +18 -0
  572. package/templates/recipes/lint-on-save.yaml +1 -2
  573. package/templates/recipes/morning-brief-slack.yaml +57 -0
  574. package/templates/recipes/morning-brief.yaml +2 -2
  575. package/templates/recipes/project-health-check.yaml +50 -0
  576. package/templates/recipes/webhook/README.md +70 -0
  577. package/templates/recipes/webhook/capture-thought.yaml +26 -0
  578. package/templates/recipes/webhook/customer-escalation.yaml +49 -0
  579. package/templates/recipes/webhook/incident-intake.yaml +46 -0
  580. package/templates/recipes/webhook/meeting-prep.yaml +48 -0
  581. package/templates/recipes/webhook/morning-brief.yaml +57 -0
package/dist/server.js CHANGED
@@ -1,14 +1,18 @@
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 { tryHandleInboxRoute } from "./inboxRoutes.js";
11
+ import { tryHandleMcpRoute } from "./mcpRoutes.js";
12
+ import { tryHandleOAuthRoute } from "./oauthRoutes.js";
10
13
  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";
14
+ import { tryHandleRecipeRoute } from "./recipeRoutes.js";
15
+ import { PACKAGE_VERSION } from "./version.js";
12
16
  const LOOPBACK_HOSTS = new Set(["localhost", "127.0.0.1", "[::1]"]);
13
17
  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
18
  function enableTcpKeepalive(ws) {
@@ -18,30 +22,10 @@ function enableTcpKeepalive(ws) {
18
22
  rawSocket.setKeepAlive(true, 60_000); // 60s TCP keepalive as defense-in-depth
19
23
  }
20
24
  }
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
- }
25
+ import { corsOrigin } from "./cors.js";
26
+ // Re-exported for streamableHttp.ts and any external callers; new code
27
+ // should import directly from "./cors.js".
28
+ export { corsOrigin };
45
29
  // Re-export canonical constant-time comparison for use in this module.
46
30
  // Implementation lives in src/crypto.ts — see there for security notes.
47
31
  const timingSafeTokenCompare = timingSafeStringEqual;
@@ -74,6 +58,8 @@ export class Server extends EventEmitter {
74
58
  oauthServer = null;
75
59
  oauthIssuerUrl = null;
76
60
  sseSubscriberCount = 0;
61
+ /** Cache for CC permission rules (30s TTL) to avoid filesystem walks on each dashboard poll */
62
+ _explainRulesCache = null;
77
63
  static MAX_SSE_SUBSCRIBERS = 20;
78
64
  /** Set by bridge to provide health data */
79
65
  healthDataFn = null;
@@ -87,24 +73,87 @@ export class Server extends EventEmitter {
87
73
  readyFn = null;
88
74
  /** Set by bridge to provide task list data (sanitized — no raw prompts) */
89
75
  tasksFn = null;
76
+ /** Set by bridge to cancel a running/pending task by id. Returns true if found. */
77
+ cancelTaskFn = null;
78
+ /** Patchwork: set by bridge to set the trust level for a recipe by name. */
79
+ setRecipeTrustFn = null;
80
+ /** Patchwork: set by bridge to generate a recipe YAML draft from a natural-language prompt. */
81
+ generateRecipeFn = null;
90
82
  /** Patchwork: set by bridge to list installed recipes for the dashboard. */
91
83
  recipesFn = null;
84
+ /** Patchwork: set by bridge to load raw recipe source content by name. */
85
+ loadRecipeContentFn = null;
86
+ /** Patchwork: set by bridge to save raw recipe source content by name. */
87
+ saveRecipeContentFn = null;
88
+ /** Patchwork: set by bridge to delete a recipe by name. */
89
+ deleteRecipeContentFn = null;
90
+ /** Patchwork: set by bridge to promote a variant recipe to the canonical name. */
91
+ promoteRecipeVariantFn = null;
92
+ /** Patchwork: set by bridge to duplicate a recipe as a variant. */
93
+ duplicateRecipeFn = null;
94
+ /** Patchwork: set by bridge to lint raw recipe content without saving. */
95
+ lintRecipeContentFn = null;
92
96
  /** Patchwork: set by bridge to save a new recipe draft to disk. */
93
97
  saveRecipeFn = null;
94
98
  /** Patchwork: set by bridge to query the recipe run audit log. */
95
99
  runsFn = null;
100
+ /** Patchwork: set by bridge to fetch a single run by seq for the detail page. */
101
+ runDetailFn = null;
102
+ /** Patchwork: set by bridge to generate a dry-run plan for a recipe by name. */
103
+ runPlanFn = null;
104
+ /** Patchwork (VD-4): mocked replay of an existing run. Returns the new
105
+ * run's seq plus any unmocked steps the caller may want to surface. */
106
+ runReplayFn = null;
96
107
  /** Patchwork: set by bridge to launch a named recipe via the orchestrator. */
97
108
  runRecipeFn = null;
98
109
  /** Patchwork: admin-controlled managed settings path (highest rule precedence). */
99
110
  managedSettingsPath = undefined;
111
+ /** Effective bridge config path to update when dashboard saves driver changes. */
112
+ bridgeConfigPath = undefined;
100
113
  /** Patchwork: live approval gate level — mutated by POST /settings, read by bridge per-session setup. */
101
114
  approvalGate = "off";
102
115
  /** Patchwork: outbound webhook URL for approval notifications (from dashboard.webhookUrl in config). */
103
116
  approvalWebhookUrl = undefined;
117
+ /** Patchwork: push relay service URL — when set, per-callId approval tokens are generated. */
118
+ pushServiceUrl = undefined;
119
+ /** Patchwork: bearer token for the push relay service. */
120
+ pushServiceToken = undefined;
121
+ /** Patchwork: public base URL of this bridge, embedded in push payloads as callback base. */
122
+ pushServiceBaseUrl = undefined;
104
123
  /** Patchwork: approval decision audit callback wired to activityLog.recordEvent. */
105
124
  onApprovalDecision = undefined;
125
+ /**
126
+ * Patchwork: activity log handle, used by approvalHttp to compute
127
+ * passive risk personalization signals (`src/approvalSignals.ts`).
128
+ * When unset, personalSignals are simply omitted from queue entries.
129
+ */
130
+ activityLog = undefined;
131
+ /**
132
+ * Patchwork: recipe-run log handle, used by approvalHttp for the
133
+ * "recipe-step trust" heuristic (h6 in src/approvalSignals.ts). When
134
+ * unset, h6 is silently skipped; the other personalSignals heuristics
135
+ * still compute as long as `activityLog` is wired.
136
+ */
137
+ recipeRunLog = undefined;
138
+ /**
139
+ * Patchwork: opt-in switch for personalSignals heuristic 10
140
+ * (time-of-day anomaly). Off by default — see config.ts. Threaded into
141
+ * routeApprovalRequest deps so the personalSignals computation honors
142
+ * the user's preference.
143
+ */
144
+ enableTimeOfDayAnomaly = false;
106
145
  /** Patchwork: set by bridge to match + fire webhook-triggered recipes. */
107
146
  webhookFn = null;
147
+ /**
148
+ * Patchwork: ring buffer of recent webhook payloads, keyed by path
149
+ * (e.g. "/incident-war-room"). The last MAX_WEBHOOK_PAYLOADS entries are
150
+ * retained per path so the dashboard can show what the recipe most
151
+ * recently received — answers "did the trigger fire? what did it send?"
152
+ * without forcing the user to dig through bridge logs. In-memory only;
153
+ * cleared on restart.
154
+ */
155
+ webhookPayloads = new Map();
156
+ static MAX_WEBHOOK_PAYLOADS = 5;
108
157
  /** Set by bridge to handle MCP Streamable HTTP sessions (POST/GET/DELETE /mcp) */
109
158
  httpMcpHandler = null;
110
159
  /** Set by bridge to subscribe a caller to real-time activity events. Returns unsubscribe fn. */
@@ -127,6 +176,7 @@ export class Server extends EventEmitter {
127
176
  sessionDetailFn = null;
128
177
  /** Set by bridge to handle POST /launch-quick-task — invokes launchQuickTask tool in-process. */
129
178
  launchQuickTaskFn = null;
179
+ setRecipeEnabledFn = null;
130
180
  /**
131
181
  * Attach an OAuth 2.0 Authorization Server.
132
182
  * When set, the bridge exposes:
@@ -181,141 +231,37 @@ export class Server extends EventEmitter {
181
231
  res.setHeader("Access-Control-Allow-Headers", "Content-Type, Authorization, Mcp-Session-Id");
182
232
  res.setHeader("Access-Control-Expose-Headers", "Mcp-Session-Id");
183
233
  }
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
- }
234
+ // DNS rebinding defense: validate Host header on every HTTP request,
235
+ // mirroring the WS upgrade handler below. Without this, attacker DNS
236
+ // can rebind a public hostname to 127.0.0.1 in the victim's browser
237
+ // and reach `/dashboard`, `/health`, `/metrics`, `/mcp`, OAuth
238
+ // endpoints, etc. with arbitrary Host headers. CORS gates browser
239
+ // *reads* of responses but does NOT gate top-level navigations or
240
+ // simple side-effect-bearing POSTs (e.g. `/oauth/authorize`).
241
+ const rawHost = req.headers.host ?? "";
242
+ const host = rawHost.startsWith("[")
243
+ ? rawHost.slice(0, rawHost.indexOf("]") + 1)
244
+ : rawHost.replace(/:\d+$/, "");
245
+ if (!host || !this.allowedHosts.has(host)) {
246
+ this.logger.warn(`Rejected HTTP request with invalid Host header: ${rawHost}`);
247
+ res.writeHead(403, { "Content-Type": "text/plain" });
248
+ res.end("Invalid Host header");
279
249
  return;
280
250
  }
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));
251
+ const parsedUrl = new URL(req.url ?? "/", "http://localhost");
252
+ // ── OAuth 2.0 endpoints (extracted to src/oauthRoutes.ts) ────────────
253
+ // Unauthenticated — must run BEFORE the bearer-auth gate.
254
+ if (tryHandleOAuthRoute(req, res, parsedUrl, {
255
+ oauthServer: this.oauthServer,
256
+ oauthIssuerUrl: this.oauthIssuerUrl,
257
+ })) {
305
258
  return;
306
259
  }
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();
260
+ // ── MCP server-card + CORS preflight (extracted to src/mcpRoutes.ts)
261
+ // Unauthenticated must run BEFORE the bearer-auth gate.
262
+ if (tryHandleMcpRoute(req, res, parsedUrl, {
263
+ extraCorsOrigins: this.extraCorsOrigins,
264
+ })) {
319
265
  return;
320
266
  }
321
267
  // Unauthenticated liveness probe — safe to expose; contains no sensitive data.
@@ -355,78 +301,57 @@ export class Server extends EventEmitter {
355
301
  res.end(JSON.stringify({ ok: true, v: PACKAGE_VERSION }));
356
302
  return;
357
303
  }
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
- })();
304
+ // ── Connector OAuth callbacks (extracted to src/connectorRoutes.ts) ──
305
+ // Unauthenticated browser redirects from vendor must run BEFORE the
306
+ // bearer-auth gate.
307
+ if (tryHandlePublicConnectorRoute(req, res, parsedUrl)) {
402
308
  return;
403
309
  }
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",
310
+ // ── /schemas/* — unauthenticated registry-derived JSON Schemas ────────
311
+ // Serves recipe.v1.json, dry-run-plan.v1.json, tools/<ns>.json so YAML-LSP
312
+ // editors can resolve `$schema:` headers against a running bridge. No
313
+ // secrets schemas are generated from the tool registry.
314
+ if (parsedUrl.pathname?.startsWith("/schemas/") && req.method === "GET") {
315
+ try {
316
+ await import("./recipes/tools/index.js");
317
+ const { generateSchemaSet } = await import("./recipes/schemaGenerator.js");
318
+ const schemas = generateSchemaSet();
319
+ const rest = parsedUrl.pathname.slice("/schemas/".length);
320
+ let body;
321
+ if (rest === "recipe.v1.json") {
322
+ body = schemas.recipe;
323
+ }
324
+ else if (rest === "dry-run-plan.v1.json") {
325
+ body = schemas.dryRunPlan;
326
+ }
327
+ else if (rest.startsWith("tools/") && rest.endsWith(".json")) {
328
+ const ns = rest.slice("tools/".length, -".json".length);
329
+ body = schemas.namespaces[ns];
330
+ }
331
+ else if (rest === "" || rest === "index.json") {
332
+ body = {
333
+ recipe: "/schemas/recipe.v1.json",
334
+ dryRunPlan: "/schemas/dry-run-plan.v1.json",
335
+ tools: Object.keys(schemas.namespaces).map((ns) => `/schemas/tools/${ns}.json`),
336
+ };
337
+ }
338
+ if (body === undefined) {
339
+ res.writeHead(404, { "Content-Type": "application/json" });
340
+ res.end(JSON.stringify({ error: `schema not found: ${rest}` }));
341
+ return;
342
+ }
343
+ res.writeHead(200, {
344
+ "Content-Type": "application/schema+json",
345
+ "Cache-Control": "public, max-age=60",
414
346
  });
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
- })();
347
+ res.end(JSON.stringify(body, null, 2));
348
+ }
349
+ catch (err) {
350
+ res.writeHead(500, { "Content-Type": "application/json" });
351
+ res.end(JSON.stringify({
352
+ error: err instanceof Error ? err.message : String(err),
353
+ }));
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
@@ -552,6 +482,83 @@ export class Server extends EventEmitter {
552
482
  }
553
483
  return;
554
484
  }
485
+ if (parsedUrl.pathname === "/traces/export" && req.method === "GET") {
486
+ void (async () => {
487
+ try {
488
+ // Accept passphrase only via header — never query string (prevents
489
+ // proxy access-log exposure and browser-history leakage).
490
+ if (parsedUrl.searchParams?.get("passphrase")) {
491
+ res.writeHead(400, { "Content-Type": "application/json" });
492
+ res.end(JSON.stringify({
493
+ error: "passphrase must be sent in the X-Trace-Passphrase header, not the URL",
494
+ }));
495
+ return;
496
+ }
497
+ const passphraseRaw = req.headers["x-trace-passphrase"] ?? null;
498
+ if (passphraseRaw !== null && passphraseRaw.length > 4096) {
499
+ res.writeHead(400, { "Content-Type": "application/json" });
500
+ res.end(JSON.stringify({
501
+ error: "passphrase too long (max 4096 chars)",
502
+ }));
503
+ return;
504
+ }
505
+ if (passphraseRaw !== null && passphraseRaw.length < 12) {
506
+ res.writeHead(400, { "Content-Type": "application/json" });
507
+ res.end(JSON.stringify({
508
+ error: "passphrase too short (min 12 chars)",
509
+ }));
510
+ return;
511
+ }
512
+ const passphrase = passphraseRaw;
513
+ const { runTracesExportToStream } = await import("./commands/tracesExport.js");
514
+ const stamp = new Date()
515
+ .toISOString()
516
+ .replace(/:/g, "-")
517
+ .replace(/\..+$/, "");
518
+ if (passphrase) {
519
+ // Encrypted export — buffer the gzip, then AES-256-GCM encrypt.
520
+ const { encryptTraceBundle } = await import("./traceEncryption.js");
521
+ const chunks = [];
522
+ const { Writable } = await import("node:stream");
523
+ const collector = new Writable({
524
+ write(chunk, _enc, cb) {
525
+ chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
526
+ cb();
527
+ },
528
+ });
529
+ await runTracesExportToStream(collector);
530
+ const plain = Buffer.concat(chunks);
531
+ const encrypted = encryptTraceBundle(plain, passphrase);
532
+ const filename = `traces-export-${stamp}.enc`;
533
+ res.writeHead(200, {
534
+ "Content-Type": "application/octet-stream",
535
+ "Content-Disposition": `attachment; filename="${filename}"`,
536
+ "Cache-Control": "no-store",
537
+ "Content-Length": String(encrypted.length),
538
+ });
539
+ res.end(encrypted);
540
+ }
541
+ else {
542
+ const filename = `traces-export-${stamp}.jsonl.gz`;
543
+ res.writeHead(200, {
544
+ "Content-Type": "application/gzip",
545
+ "Content-Disposition": `attachment; filename="${filename}"`,
546
+ "Cache-Control": "no-store",
547
+ });
548
+ await runTracesExportToStream(res);
549
+ }
550
+ }
551
+ catch (err) {
552
+ if (!res.headersSent) {
553
+ res.writeHead(500, { "Content-Type": "application/json" });
554
+ res.end(JSON.stringify({
555
+ error: err instanceof Error ? err.message : String(err),
556
+ }));
557
+ }
558
+ }
559
+ })();
560
+ return;
561
+ }
555
562
  if (parsedUrl.pathname === "/analytics" && req.method === "GET") {
556
563
  try {
557
564
  const wh = parsedUrl.searchParams.get("windowHours");
@@ -681,6 +688,28 @@ export class Server extends EventEmitter {
681
688
  }
682
689
  return;
683
690
  }
691
+ const cancelMatch = parsedUrl.pathname?.match(/^\/tasks\/([^/]+)\/cancel$/);
692
+ if (cancelMatch && req.method === "POST") {
693
+ const taskId = cancelMatch[1];
694
+ try {
695
+ const found = this.cancelTaskFn?.(taskId) ?? false;
696
+ if (!found) {
697
+ res.writeHead(404, { "Content-Type": "application/json" });
698
+ res.end(JSON.stringify({ error: "task not found or already terminal" }));
699
+ }
700
+ else {
701
+ res.writeHead(200, { "Content-Type": "application/json" });
702
+ res.end(JSON.stringify({ ok: true }));
703
+ }
704
+ }
705
+ catch (err) {
706
+ res.writeHead(500, { "Content-Type": "application/json" });
707
+ res.end(JSON.stringify({
708
+ error: err instanceof Error ? err.message : String(err),
709
+ }));
710
+ }
711
+ return;
712
+ }
684
713
  if (parsedUrl.pathname?.startsWith("/hooks/") && req.method === "POST") {
685
714
  const hookPath = parsedUrl.pathname.substring("/hooks".length);
686
715
  const chunks = [];
@@ -713,531 +742,191 @@ export class Server extends EventEmitter {
713
742
  : result.error === "not_found"
714
743
  ? 404
715
744
  : 400;
745
+ // Record in ring buffer so the dashboard can show "last
746
+ // payload" per recipe. Skip not_found so unknown paths don't
747
+ // pollute the buffer with garbage / scanner traffic.
748
+ if (result.error !== "not_found") {
749
+ const existing = this.webhookPayloads.get(hookPath) ?? [];
750
+ existing.unshift({
751
+ receivedAt: Date.now(),
752
+ payload,
753
+ ok: result.ok,
754
+ error: result.error,
755
+ taskId: result.taskId,
756
+ recipeName: result.name,
757
+ });
758
+ if (existing.length > Server.MAX_WEBHOOK_PAYLOADS) {
759
+ existing.length = Server.MAX_WEBHOOK_PAYLOADS;
760
+ }
761
+ this.webhookPayloads.set(hookPath, existing);
762
+ }
716
763
  res.writeHead(status, { "Content-Type": "application/json" });
717
764
  res.end(JSON.stringify(result));
718
765
  })();
719
766
  });
720
767
  return;
721
768
  }
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();
742
- }
743
- else {
744
- res.writeHead(result.status, {
745
- "Content-Type": result.contentType ?? "application/json",
746
- });
747
- res.end(result.body);
748
- }
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
- })();
777
- return;
778
- }
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",
786
- });
787
- res.end(result.body);
788
- })();
789
- return;
790
- }
791
- // ── GitHub MCP connector routes ─────────────────────────────────────
792
- if (parsedUrl.pathname === "/connections/github/auth" &&
793
- 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
- })();
808
- return;
809
- }
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
- })();
820
- return;
821
- }
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
- })();
832
- return;
833
- }
834
- // ── Sentry MCP connector routes ─────────────────────────────────────
835
- if (parsedUrl.pathname === "/connections/sentry/auth" &&
836
- 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
- })();
851
- return;
852
- }
853
- if (parsedUrl.pathname === "/connections/sentry/callback" &&
769
+ if (parsedUrl.pathname?.startsWith("/webhook-payloads/") &&
854
770
  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
- })();
866
- return;
867
- }
868
- if (parsedUrl.pathname === "/connections/sentry/test" &&
869
- 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
- })();
771
+ const hookPath = parsedUrl.pathname.substring("/webhook-payloads".length);
772
+ const entries = this.webhookPayloads.get(hookPath) ?? [];
773
+ res.writeHead(200, { "Content-Type": "application/json" });
774
+ res.end(JSON.stringify({ path: hookPath, entries }));
948
775
  return;
949
776
  }
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();
777
+ // Activity-based automation suggestions (Phase 3 §4). Read-only
778
+ // pattern-mining over the running bridge's activity log + recipe
779
+ // run history. Same logic the `patchwork suggest` CLI calls — this
780
+ // exposes it to the dashboard so suggestions live where users look.
781
+ if (parsedUrl.pathname === "/suggestions" && req.method === "GET") {
782
+ if (!this.activityLog) {
783
+ res.writeHead(503, { "Content-Type": "application/json" });
784
+ res.end(JSON.stringify({
785
+ error: "activity log not wired — bridge probably not in a configuration that records activity",
786
+ }));
787
+ return;
958
788
  }
959
- else {
960
- res.writeHead(result.status, { "Content-Type": result.contentType ?? "application/json" });
961
- res.end(result.body);
789
+ const sinceDaysParam = parsedUrl.searchParams?.get("sinceDays");
790
+ const sinceDays = sinceDaysParam !== null && sinceDaysParam !== undefined
791
+ ? Number.parseInt(sinceDaysParam, 10)
792
+ : undefined;
793
+ const { computeAutomationSuggestions } = await import("./automationSuggestions.js");
794
+ const opts = {
795
+ activityLog: this.activityLog,
796
+ };
797
+ if (this.recipeRunLog)
798
+ opts.recipeRunLog = this.recipeRunLog;
799
+ if (sinceDays !== undefined && Number.isFinite(sinceDays)) {
800
+ opts.activitySinceMs = sinceDays * 24 * 60 * 60 * 1000;
962
801
  }
802
+ const suggestions = computeAutomationSuggestions(opts);
803
+ res.writeHead(200, { "Content-Type": "application/json" });
804
+ res.end(JSON.stringify({
805
+ suggestions,
806
+ generatedAt: new Date().toISOString(),
807
+ }));
963
808
  return;
964
809
  }
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
- })();
810
+ // Approval insights aggregate approval-decision history for Phase 3 §3
811
+ // passive risk personalization. Read-only; no state changes.
812
+ if (parsedUrl.pathname === "/approval-insights" && req.method === "GET") {
813
+ if (!this.activityLog) {
814
+ res.writeHead(503, { "Content-Type": "application/json" });
815
+ res.end(JSON.stringify({
816
+ error: "activity log not wired",
817
+ }));
818
+ return;
819
+ }
820
+ const { computeApprovalInsights } = await import("./approvalInsights.js");
821
+ const result = computeApprovalInsights(this.activityLog);
822
+ res.writeHead(200, { "Content-Type": "application/json" });
823
+ res.end(JSON.stringify(result));
1000
824
  return;
1001
825
  }
1002
- if (parsedUrl.pathname === "/connections/google-calendar/callback" &&
826
+ // Decision replay — Phase 3 §2. Re-evaluates historical approval
827
+ // decisions against the current CC policy. Read-only; no side effects.
828
+ if (parsedUrl.pathname === "/approval-insights/replay" &&
1003
829
  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
- })();
830
+ if (!this.activityLog) {
831
+ res.writeHead(503, { "Content-Type": "application/json" });
832
+ res.end(JSON.stringify({ error: "activity log not wired" }));
833
+ return;
834
+ }
835
+ const sinceDaysParam = parsedUrl.searchParams?.get("sinceDays");
836
+ const sinceDays = sinceDaysParam !== null && sinceDaysParam !== undefined
837
+ ? Number.parseInt(sinceDaysParam, 10)
838
+ : 7;
839
+ const sinceMs = Number.isFinite(sinceDays)
840
+ ? Date.now() - sinceDays * 24 * 60 * 60 * 1000
841
+ : 0;
842
+ const { computeDecisionReplay } = await import("./decisionReplay.js");
843
+ const result = computeDecisionReplay(this.activityLog, {
844
+ workspace: process.cwd(),
845
+ sinceMs,
1165
846
  });
847
+ res.writeHead(200, { "Content-Type": "application/json" });
848
+ res.end(JSON.stringify(result));
1166
849
  return;
1167
850
  }
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 }));
851
+ // Rule explanation returns which CC permission rule matched a tool call
852
+ // and why approval was required. Phase 1 §2 Delegation Policy UX.
853
+ if (parsedUrl.pathname === "/approval-insights/explain" &&
854
+ req.method === "GET") {
855
+ const tool = parsedUrl.searchParams?.get("tool") ?? "";
856
+ const specifier = parsedUrl.searchParams?.get("specifier") ?? undefined;
857
+ if (!tool) {
858
+ res.writeHead(400, { "Content-Type": "application/json" });
859
+ res.end(JSON.stringify({ error: "tool param required" }));
860
+ return;
1187
861
  }
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
- }));
862
+ const { loadCcPermissionsAttributed, explainRules } = await import("./ccPermissions.js");
863
+ // Cache rules for 30 s — loadCcPermissionsAttributed walks the
864
+ // filesystem and this endpoint can be polled by the dashboard.
865
+ const now = Date.now();
866
+ if (!this._explainRulesCache ||
867
+ now - this._explainRulesCache.at > 30_000) {
868
+ this._explainRulesCache = {
869
+ at: now,
870
+ rules: loadCcPermissionsAttributed(process.cwd()),
871
+ };
1193
872
  }
873
+ const explanation = explainRules(tool, specifier || undefined, this._explainRulesCache.rules);
874
+ res.writeHead(200, { "Content-Type": "application/json" });
875
+ res.end(JSON.stringify({ tool, specifier: specifier ?? null, explanation }));
1194
876
  return;
1195
877
  }
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
- });
878
+ // Reversible-refactoring surface list active staged transactions
879
+ // (Phase 1 §3 dashboard ask). Read-only metadata for the dashboard
880
+ // /transactions page; no file contents leave the bridge.
881
+ if (parsedUrl.pathname === "/transactions" && req.method === "GET") {
882
+ const { listActiveTransactions } = await import("./tools/transaction.js");
883
+ const transactions = listActiveTransactions();
884
+ res.writeHead(200, { "Content-Type": "application/json" });
885
+ res.end(JSON.stringify({ transactions }));
1227
886
  return;
1228
887
  }
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
- }
888
+ // Discard-only we deliberately do NOT expose commit via HTTP because
889
+ // the commit handler needs per-workspace context wired through
890
+ // createTransactionTools(). Rollback is pure-memory and workspace-
891
+ // agnostic, safe to expose. Commit from the agent side via MCP.
892
+ if (parsedUrl.pathname?.match(/^\/transactions\/[^/]+\/rollback$/) &&
893
+ req.method === "POST") {
894
+ const id = parsedUrl.pathname.split("/")[2] ?? "";
895
+ const { rollbackTransactionById } = await import("./tools/transaction.js");
896
+ const ok = id !== "" && rollbackTransactionById(id);
897
+ res.writeHead(ok ? 200 : 404, { "Content-Type": "application/json" });
898
+ res.end(JSON.stringify(ok
899
+ ? { ok: true, transactionId: id }
900
+ : { ok: false, error: "transaction not found" }));
901
+ return;
902
+ }
903
+ // ── Connector routes (extracted to src/connectorRoutes.ts) ──────────
904
+ if (tryHandleConnectorRoute(req, res, parsedUrl)) {
905
+ return;
906
+ }
907
+ // ── Inbox routes (extracted to src/inboxRoutes.ts) ───────────────────
908
+ if (tryHandleInboxRoute(req, res, parsedUrl)) {
909
+ return;
910
+ }
911
+ // ── Recipe / runs / templates routes (extracted to src/recipeRoutes.ts) ─
912
+ if (tryHandleRecipeRoute(req, res, parsedUrl, {
913
+ setRecipeTrustFn: this.setRecipeTrustFn,
914
+ generateRecipeFn: this.generateRecipeFn,
915
+ recipesFn: this.recipesFn,
916
+ loadRecipeContentFn: this.loadRecipeContentFn,
917
+ saveRecipeContentFn: this.saveRecipeContentFn,
918
+ deleteRecipeContentFn: this.deleteRecipeContentFn,
919
+ duplicateRecipeFn: this.duplicateRecipeFn,
920
+ promoteRecipeVariantFn: this.promoteRecipeVariantFn,
921
+ lintRecipeContentFn: this.lintRecipeContentFn,
922
+ saveRecipeFn: this.saveRecipeFn,
923
+ setRecipeEnabledFn: this.setRecipeEnabledFn,
924
+ runsFn: this.runsFn,
925
+ runDetailFn: this.runDetailFn,
926
+ runPlanFn: this.runPlanFn,
927
+ runReplayFn: this.runReplayFn,
928
+ runRecipeFn: this.runRecipeFn,
929
+ })) {
1241
930
  return;
1242
931
  }
1243
932
  const sessionDetailMatch = /^\/sessions\/([A-Za-z0-9-]+)$/.exec(parsedUrl.pathname);
@@ -1286,8 +975,11 @@ export class Server extends EventEmitter {
1286
975
  req.on("end", () => {
1287
976
  try {
1288
977
  const body = JSON.parse(Buffer.concat(chunks).toString("utf-8"));
1289
- const raw = body.webhookUrl?.trim() ?? "";
1290
- if (raw !== "" && !/^https:\/\/.+/.test(raw)) {
978
+ const hasWebhookUpdate = body.webhookUrl !== undefined;
979
+ const raw = hasWebhookUpdate
980
+ ? (body.webhookUrl?.trim() ?? "")
981
+ : undefined;
982
+ if (raw !== undefined && raw !== "" && !/^https:\/\/.+/.test(raw)) {
1291
983
  res.writeHead(400, { "Content-Type": "application/json" });
1292
984
  res.end(JSON.stringify({ error: "webhookUrl must be HTTPS" }));
1293
985
  return;
@@ -1306,19 +998,112 @@ export class Server extends EventEmitter {
1306
998
  const configPath = patchworkConfigPath();
1307
999
  const cfg = loadPatchworkConfig(configPath);
1308
1000
  cfg.dashboard = {
1309
- port: cfg.dashboard?.port ?? 3000,
1001
+ port: cfg.dashboard?.port ?? 3200,
1310
1002
  requireApproval: cfg.dashboard?.requireApproval ?? ["high"],
1311
1003
  pushNotifications: cfg.dashboard?.pushNotifications ?? false,
1312
- webhookUrl: raw || undefined,
1004
+ webhookUrl: hasWebhookUpdate
1005
+ ? raw || undefined
1006
+ : cfg.dashboard?.webhookUrl,
1313
1007
  };
1314
1008
  if (gateRaw !== undefined) {
1315
1009
  cfg.approvalGate = gateRaw;
1316
1010
  this.approvalGate = gateRaw;
1317
1011
  }
1012
+ // h10 toggle: must be boolean if present. Persists to
1013
+ // ~/.patchwork/config.json AND live-mutates the Server
1014
+ // field so the next /approvals POST honors it without
1015
+ // needing a bridge restart.
1016
+ if (body.enableTimeOfDayAnomaly !== undefined) {
1017
+ if (typeof body.enableTimeOfDayAnomaly !== "boolean") {
1018
+ res.writeHead(400, { "Content-Type": "application/json" });
1019
+ res.end(JSON.stringify({
1020
+ error: "enableTimeOfDayAnomaly must be a boolean",
1021
+ }));
1022
+ return;
1023
+ }
1024
+ cfg.enableTimeOfDayAnomaly = body.enableTimeOfDayAnomaly;
1025
+ this.enableTimeOfDayAnomaly = body.enableTimeOfDayAnomaly;
1026
+ }
1027
+ const driverRaw = body.driver;
1028
+ if (driverRaw !== undefined) {
1029
+ const validDrivers = [
1030
+ "subprocess",
1031
+ "api",
1032
+ "openai",
1033
+ "grok",
1034
+ "gemini",
1035
+ "none",
1036
+ ];
1037
+ if (!validDrivers.includes(driverRaw)) {
1038
+ res.writeHead(400, { "Content-Type": "application/json" });
1039
+ res.end(JSON.stringify({
1040
+ error: `driver must be one of: ${validDrivers.join(", ")}`,
1041
+ }));
1042
+ return;
1043
+ }
1044
+ const driver = driverRaw;
1045
+ cfg.driver = driver;
1046
+ saveBridgeConfigDriver(driver, this.bridgeConfigPath);
1047
+ }
1048
+ if (body.model !== undefined) {
1049
+ const validModels = [
1050
+ "claude",
1051
+ "openai",
1052
+ "gemini",
1053
+ "grok",
1054
+ "local",
1055
+ ];
1056
+ if (!validModels.includes(body.model)) {
1057
+ res.writeHead(400, { "Content-Type": "application/json" });
1058
+ res.end(JSON.stringify({
1059
+ error: `model must be one of: ${validModels.join(", ")}`,
1060
+ }));
1061
+ return;
1062
+ }
1063
+ cfg.model = body.model;
1064
+ if (body.model === "local") {
1065
+ if (body.localEndpoint !== undefined)
1066
+ cfg.localEndpoint = body.localEndpoint.trim() || undefined;
1067
+ if (body.localModel !== undefined)
1068
+ cfg.localModel = body.localModel.trim() || undefined;
1069
+ }
1070
+ }
1071
+ if (body.apiKey) {
1072
+ const { provider, key } = body.apiKey;
1073
+ const validProviders = ["anthropic", "openai", "google", "xai"];
1074
+ if (!validProviders.includes(provider) ||
1075
+ typeof key !== "string") {
1076
+ res.writeHead(400, { "Content-Type": "application/json" });
1077
+ res.end(JSON.stringify({ error: "Invalid apiKey provider or key" }));
1078
+ return;
1079
+ }
1080
+ cfg.apiKeys = { ...cfg.apiKeys, [provider]: key || undefined };
1081
+ }
1318
1082
  savePatchworkConfig(cfg, configPath);
1319
- this.approvalWebhookUrl = raw || undefined;
1083
+ if (hasWebhookUpdate) {
1084
+ this.approvalWebhookUrl = raw || undefined;
1085
+ }
1086
+ if (body.pushServiceUrl !== undefined) {
1087
+ const pushUrl = body.pushServiceUrl.trim();
1088
+ if (pushUrl && !pushUrl.startsWith("https://")) {
1089
+ res.writeHead(400, { "Content-Type": "application/json" });
1090
+ res.end(JSON.stringify({ error: "pushServiceUrl must be HTTPS" }));
1091
+ return;
1092
+ }
1093
+ this.pushServiceUrl = pushUrl || undefined;
1094
+ }
1095
+ if (body.pushServiceToken !== undefined) {
1096
+ this.pushServiceToken = body.pushServiceToken.trim() || undefined;
1097
+ }
1098
+ if (body.pushServiceBaseUrl !== undefined) {
1099
+ this.pushServiceBaseUrl =
1100
+ body.pushServiceBaseUrl.trim() || undefined;
1101
+ }
1102
+ const restartRequired = driverRaw !== undefined ||
1103
+ body.apiKey !== undefined ||
1104
+ body.model !== undefined;
1320
1105
  res.writeHead(200, { "Content-Type": "application/json" });
1321
- res.end(JSON.stringify({ ok: true }));
1106
+ res.end(JSON.stringify({ ok: true, restartRequired }));
1322
1107
  }
1323
1108
  catch (err) {
1324
1109
  res.writeHead(400, { "Content-Type": "application/json" });
@@ -1382,6 +1167,11 @@ export class Server extends EventEmitter {
1382
1167
  }
1383
1168
  return;
1384
1169
  }
1170
+ // SSE stream for live approval queue updates.
1171
+ if (parsedUrl.pathname === "/approvals/stream" && req.method === "GET") {
1172
+ handleApprovalsStream(res, { queue: getApprovalQueue() }, parsedUrl.searchParams.get("session"));
1173
+ return;
1174
+ }
1385
1175
  // Patchwork approval surface — PreToolUse hook + dashboard approve/reject.
1386
1176
  // Bearer auth already checked above.
1387
1177
  if (parsedUrl.pathname === "/approvals" ||
@@ -1407,6 +1197,7 @@ export class Server extends EventEmitter {
1407
1197
  path: parsedUrl.pathname,
1408
1198
  body: parsedBody,
1409
1199
  query: parsedUrl.searchParams,
1200
+ approvalToken: req.headers["x-approval-token"],
1410
1201
  }, {
1411
1202
  queue: getApprovalQueue(),
1412
1203
  workspace: process.cwd(),
@@ -1414,6 +1205,18 @@ export class Server extends EventEmitter {
1414
1205
  onDecision: this.onApprovalDecision,
1415
1206
  webhookUrl: this.approvalWebhookUrl,
1416
1207
  approvalGate: this.approvalGate,
1208
+ pushServiceUrl: this.pushServiceUrl,
1209
+ pushServiceToken: this.pushServiceToken,
1210
+ pushServiceBaseUrl: this.pushServiceBaseUrl,
1211
+ activityLog: this.activityLog,
1212
+ // RecipeRunLog satisfies RecipeRunQuerier structurally
1213
+ // — the cast bridges TS contravariance: RecipeRunQuerier's
1214
+ // narrow query interface (`status?: string`) is deliberately
1215
+ // loose so tests can mock it; RecipeRunLog's stricter
1216
+ // RunStatus union is a strict subset and fails the param
1217
+ // contravariance check despite being safe at runtime.
1218
+ recipeRunLog: this.recipeRunLog,
1219
+ enableTimeOfDayAnomaly: this.enableTimeOfDayAnomaly,
1417
1220
  });
1418
1221
  res.writeHead(result.status, {
1419
1222
  "Content-Type": "application/json",