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
@@ -0,0 +1,1345 @@
1
+ /**
2
+ * Recipe + run-audit route dispatcher — extracted from src/server.ts.
3
+ *
4
+ * Owns 16 routes covering the recipe authoring loop (CRUD + lint + run),
5
+ * the run-audit log (`/runs`, `/runs/:seq`, replay, plan), the public
6
+ * template registry (`/templates`), and recipe installation
7
+ * (`/recipes/install`). Plus the `/activation-metrics` siblings that
8
+ * read the same audit log.
9
+ *
10
+ * DI shape: handlers depend on 12 nullable callbacks the bridge wires
11
+ * onto the Server instance post-construction (`runRecipeFn`,
12
+ * `recipesFn`, etc.). They're passed as a `RecipeRouteDeps` struct
13
+ * matching the pattern from oauthRoutes.ts and mcpRoutes.ts.
14
+ *
15
+ * Module-level state: the `/templates` 5-minute cache used to live as
16
+ * `_templatesCache`/`_templatesCacheTs` instance fields on Server.
17
+ * Lifetime is process-wide either way (Server is a singleton in
18
+ * practice), so hoisting to module scope here is equivalent and avoids
19
+ * threading a mutable holder through `deps`.
20
+ *
21
+ * Mechanical lift: handler bodies are byte-identical save for
22
+ * `deps.<fn>` replacing `this.<fn>` and module-scoped cache vars
23
+ * replacing `this._templatesCache`. A few routes that previously used
24
+ * `await` directly in their async parent closure are wrapped in
25
+ * `void (async () => {...})()` so this module can return boolean
26
+ * synchronously — same micro-task tradeoff documented in
27
+ * connectorRoutes.ts.
28
+ */
29
+ import os from "node:os";
30
+ import path from "node:path";
31
+ import { computeSummary as computeActivationSummary, loadMetrics as loadActivationMetrics, } from "./activationMetrics.js";
32
+ import { consumeToken, refillBucket, } from "./fp/tokenBucket.js";
33
+ import { validateSafeUrl } from "./ssrfGuard.js";
34
+ // 5-minute cache of the public template registry from the patchworkos/recipes
35
+ // GitHub repo. Process-wide; hoisted out of Server class state.
36
+ let templatesCache = null;
37
+ let templatesCacheTs = 0;
38
+ /**
39
+ * Per-process token bucket guarding `/recipes/generate`. Every call to the
40
+ * route enqueues a Claude subprocess via the orchestrator — without a cap a
41
+ * scripted attacker holding a bridge token can DoS the queue or run up
42
+ * subscription costs. The bridge's existing `--tool-rate-limit` token bucket
43
+ * is per-session and gates MCP tool calls, not HTTP routes; this is a
44
+ * separate, route-scoped cap.
45
+ *
46
+ * Default: 10 req/min — generous for a feature that takes 5–10s per call.
47
+ * Process-wide because the bridge HTTP transport doesn't expose a stable
48
+ * per-caller identity beyond "valid bearer token", and the Claude
49
+ * subprocess queue is the actual bottleneck regardless of caller.
50
+ *
51
+ * Exported `_resetGenerateRateLimitForTests` lets tests start each case
52
+ * with a full bucket.
53
+ */
54
+ const RECIPE_GENERATE_LIMIT_PER_MIN = 10;
55
+ let recipeGenerateBucket = {
56
+ tokens: RECIPE_GENERATE_LIMIT_PER_MIN,
57
+ lastRefill: 0,
58
+ };
59
+ export function _resetGenerateRateLimitForTests() {
60
+ recipeGenerateBucket = {
61
+ tokens: RECIPE_GENERATE_LIMIT_PER_MIN,
62
+ lastRefill: 0,
63
+ };
64
+ }
65
+ // G-security R2 C-3 / I-3 / F-02: HTTP `vars` validation.
66
+ //
67
+ // The post-render path jail in `src/recipes/resolveRecipePath.ts` is the
68
+ // actual defense against template-driven traversal — but rejecting bad
69
+ // vars at the HTTP layer is cheaper and gives the caller a precise 400
70
+ // instead of a generic 500 from the runner. Validation rules:
71
+ //
72
+ // - keys — `/^[a-zA-Z_][a-zA-Z0-9_]{0,63}$/` (identifier-ish, ≤64)
73
+ // - values — `/^[\w\-. :+@,]+$/u` (no `/`, no `..`, no
74
+ // `~`, no control chars)
75
+ // - type — strings only; numbers/objects/arrays → 400 (type-strict
76
+ // per R3 amendment 4 / I-3, prevents JSON.stringify smuggling
77
+ // a `..` segment into a coerced value at render time).
78
+ const VARS_KEY_RE = /^[a-zA-Z_][a-zA-Z0-9_]{0,63}$/;
79
+ const VARS_VALUE_RE = /^[\w\-. :+@,]+$/u;
80
+ /** Validate the HTTP-supplied `vars` object. Returns null on success. */
81
+ export function validateRecipeVars(vars) {
82
+ if (vars == null)
83
+ return null;
84
+ if (typeof vars !== "object" || Array.isArray(vars)) {
85
+ return {
86
+ ok: false,
87
+ error: "vars must be a plain object of string→string entries",
88
+ field: "type",
89
+ };
90
+ }
91
+ for (const [key, value] of Object.entries(vars)) {
92
+ if (!VARS_KEY_RE.test(key)) {
93
+ return {
94
+ ok: false,
95
+ error: `vars key "${key}" must match /^[a-zA-Z_][a-zA-Z0-9_]{0,63}$/`,
96
+ field: "key",
97
+ offendingKey: key,
98
+ };
99
+ }
100
+ if (typeof value !== "string") {
101
+ return {
102
+ ok: false,
103
+ error: `vars["${key}"] must be a string (got ${Array.isArray(value) ? "array" : typeof value})`,
104
+ field: "type",
105
+ offendingKey: key,
106
+ };
107
+ }
108
+ if (value.length === 0 || value.length > 1024) {
109
+ return {
110
+ ok: false,
111
+ error: `vars["${key}"] must be a non-empty string ≤ 1024 chars`,
112
+ field: "value",
113
+ offendingKey: key,
114
+ };
115
+ }
116
+ if (!VARS_VALUE_RE.test(value)) {
117
+ return {
118
+ ok: false,
119
+ error: `vars["${key}"] contains disallowed characters (no "/", "..", "~", or control chars)`,
120
+ field: "value",
121
+ offendingKey: key,
122
+ };
123
+ }
124
+ }
125
+ return null;
126
+ }
127
+ // ---------------------------------------------------------------------
128
+ // BEGIN A-PR2 EDIT BLOCK — body-cap helpers (dogfood R2 M-1 / F-08)
129
+ //
130
+ // Per-route caps; install is the strictest because the request only carries
131
+ // a single `source` field. See PR description for sizing rationale.
132
+ // Coordination note: A-PR1 also touches `recipeRoutes.ts` for `vars`
133
+ // validation; the helper APIs here are exclusively A-PR2's.
134
+ // ---------------------------------------------------------------------
135
+ export const RECIPE_ROUTE_BODY_CAPS = {
136
+ /** /recipes/install — `{ source: string }` body. */
137
+ install: 4 * 1024,
138
+ /** /recipes/generate — NL prompt. */
139
+ generate: 4 * 1024,
140
+ /** /recipes/:name/run + /recipes/run — vars envelope. */
141
+ run: 32 * 1024,
142
+ /** /recipes (POST), PUT/PATCH /recipes/:name, /recipes/lint — yaml content. */
143
+ content: 256 * 1024,
144
+ };
145
+ /**
146
+ * Read an HTTP request body up to `max` bytes. Returns the raw string or
147
+ * a `too_large` code. Used directly by routes whose handlers parse the
148
+ * body themselves (e.g. connector token-paste handlers); also used as
149
+ * the byte-collection layer for `readJsonBody`.
150
+ *
151
+ * Bytes are accumulated into a single Buffer so the helper can enforce
152
+ * the cap incrementally — a 1 GB upload is rejected after the first
153
+ * overflowing chunk rather than after the full body lands in memory.
154
+ * On overflow the stream is drained (not destroyed) so the route can
155
+ * write 413 cleanly.
156
+ */
157
+ export function readBodyWithCap(req, max) {
158
+ return new Promise((resolve) => {
159
+ const chunks = [];
160
+ let total = 0;
161
+ let aborted = false;
162
+ const onData = (chunk) => {
163
+ if (aborted)
164
+ return;
165
+ total += chunk.byteLength;
166
+ if (total > max) {
167
+ aborted = true;
168
+ // Resolve immediately so the route can write 413; do NOT destroy the
169
+ // socket here — destroying mid-upload races with the response write
170
+ // and the client sees EPIPE/ECONNRESET before reading the body.
171
+ // Subsequent chunks land in `onData` again but the `aborted` guard
172
+ // discards them, draining the upload until the client emits `end`.
173
+ resolve({ ok: false, code: "too_large" });
174
+ // Force the underlying stream to keep flowing so buffered upload
175
+ // data drains naturally. Without this Node may pause the stream
176
+ // when nothing is consuming chunks, leaving the socket half-open.
177
+ try {
178
+ req.resume();
179
+ }
180
+ catch {
181
+ // best-effort
182
+ }
183
+ return;
184
+ }
185
+ chunks.push(chunk);
186
+ };
187
+ const onEnd = () => {
188
+ if (aborted)
189
+ return;
190
+ resolve({ ok: true, body: Buffer.concat(chunks).toString("utf-8") });
191
+ };
192
+ const onError = () => {
193
+ if (aborted)
194
+ return;
195
+ aborted = true;
196
+ // Treat aborted/error mid-stream as a malformed read. Callers that
197
+ // care about the distinction can check the `code` on the
198
+ // readJsonBody result; here we collapse to "too_large" to keep
199
+ // the type narrow — in practice the network-error path is rare
200
+ // and either response is 4xx.
201
+ resolve({ ok: false, code: "too_large" });
202
+ };
203
+ req.on("data", onData);
204
+ req.once("end", onEnd);
205
+ req.once("error", onError);
206
+ });
207
+ }
208
+ /**
209
+ * Read an HTTP request body up to `max` bytes, parse as JSON, return result.
210
+ *
211
+ * Returns one of three discriminated shapes:
212
+ * - `{ ok: true, value }` — body parsed successfully.
213
+ * - `{ ok: false, code: "too_large" }` — body exceeded `max`; request was
214
+ * destroyed eagerly and the response should be 413.
215
+ * - `{ ok: false, code: "invalid_json" }` — body was valid bytes but failed
216
+ * `JSON.parse`; response should be 400.
217
+ */
218
+ export async function readJsonBody(req, max) {
219
+ const read = await readBodyWithCap(req, max);
220
+ if (!read.ok)
221
+ return { ok: false, code: "too_large" };
222
+ if (read.body.length === 0) {
223
+ // Empty bodies are passed through as `undefined`; callers decide
224
+ // whether that's an error (most parse `{...}` immediately).
225
+ return { ok: true, value: undefined };
226
+ }
227
+ try {
228
+ return { ok: true, value: JSON.parse(read.body) };
229
+ }
230
+ catch {
231
+ return { ok: false, code: "invalid_json" };
232
+ }
233
+ }
234
+ /**
235
+ * Standard 413 helper used by the routes when `readJsonBody` overflows.
236
+ *
237
+ * Note: we do NOT destroy the underlying socket — `res.end()` is sufficient.
238
+ * Destroying mid-upload is fragile across platforms (macOS races
239
+ * EPIPE/ECONNRESET to the client before the 413 body is delivered).
240
+ * The matching `readJsonBody` no-op-data drain keeps the upload flowing
241
+ * until the client emits `end`, so the server returns the response cleanly.
242
+ */
243
+ export function respond413(res, max) {
244
+ res.writeHead(413, { "Content-Type": "application/json" });
245
+ res.end(JSON.stringify({
246
+ ok: false,
247
+ error: `Request body exceeds ${max}-byte limit`,
248
+ code: "body_too_large",
249
+ }));
250
+ }
251
+ /** Standard 400 helper for malformed JSON. */
252
+ function respondInvalidJson(res) {
253
+ res.writeHead(400, { "Content-Type": "application/json" });
254
+ res.end(JSON.stringify({ ok: false, error: "Invalid JSON body" }));
255
+ }
256
+ /**
257
+ * Try to handle a recipe / run-audit / template route. Returns true if
258
+ * the route was dispatched (caller should `return` from the request
259
+ * handler), false if no route matched.
260
+ *
261
+ * Must be called AFTER bearer-auth — none of these routes are public.
262
+ */
263
+ export function tryHandleRecipeRoute(req, res, parsedUrl, deps) {
264
+ const recipeNameRunMatch = req.method === "POST"
265
+ ? /^\/recipes\/([^/]+)\/run$/.exec(parsedUrl.pathname)
266
+ : null;
267
+ if (recipeNameRunMatch) {
268
+ // A-PR2: bounded JSON read at RECIPE_ROUTE_BODY_CAPS.run (32 KB).
269
+ const nameFromPath = decodeURIComponent(recipeNameRunMatch[1] ?? "");
270
+ void (async () => {
271
+ const parsedBody = await readJsonBody(req, RECIPE_ROUTE_BODY_CAPS.run);
272
+ if (!parsedBody.ok) {
273
+ if (parsedBody.code === "too_large") {
274
+ respond413(res, RECIPE_ROUTE_BODY_CAPS.run);
275
+ }
276
+ else {
277
+ respondInvalidJson(res);
278
+ }
279
+ return;
280
+ }
281
+ try {
282
+ const parsed = parsedBody.value ?? {};
283
+ const varsRaw = parsed.vars ?? parsed.inputs;
284
+ const varsErr = validateRecipeVars(varsRaw);
285
+ if (varsErr) {
286
+ res.writeHead(400, { "Content-Type": "application/json" });
287
+ res.end(JSON.stringify(varsErr));
288
+ return;
289
+ }
290
+ const vars = varsRaw && typeof varsRaw === "object" && !Array.isArray(varsRaw)
291
+ ? varsRaw
292
+ : undefined;
293
+ if (!deps.runRecipeFn) {
294
+ res.writeHead(503, { "Content-Type": "application/json" });
295
+ res.end(JSON.stringify({
296
+ ok: false,
297
+ error: "Recipe execution unavailable — requires --claude-driver subprocess",
298
+ }));
299
+ return;
300
+ }
301
+ const result = await deps.runRecipeFn(nameFromPath, vars);
302
+ res.writeHead(result.ok ? 200 : 400, {
303
+ "Content-Type": "application/json",
304
+ });
305
+ res.end(JSON.stringify(result));
306
+ }
307
+ catch {
308
+ respondInvalidJson(res);
309
+ }
310
+ })();
311
+ return true;
312
+ }
313
+ if (parsedUrl.pathname === "/recipes/run" && req.method === "POST") {
314
+ // A-PR2: bounded JSON read at RECIPE_ROUTE_BODY_CAPS.run (32 KB).
315
+ void (async () => {
316
+ const parsedBody = await readJsonBody(req, RECIPE_ROUTE_BODY_CAPS.run);
317
+ if (!parsedBody.ok) {
318
+ if (parsedBody.code === "too_large") {
319
+ respond413(res, RECIPE_ROUTE_BODY_CAPS.run);
320
+ }
321
+ else {
322
+ respondInvalidJson(res);
323
+ }
324
+ return;
325
+ }
326
+ try {
327
+ const parsed = parsedBody.value ?? {};
328
+ const name = parsed.name;
329
+ const varsErr = validateRecipeVars(parsed.vars);
330
+ if (varsErr) {
331
+ res.writeHead(400, { "Content-Type": "application/json" });
332
+ res.end(JSON.stringify(varsErr));
333
+ return;
334
+ }
335
+ const vars = parsed.vars &&
336
+ typeof parsed.vars === "object" &&
337
+ !Array.isArray(parsed.vars)
338
+ ? parsed.vars
339
+ : undefined;
340
+ if (typeof name !== "string" || !name) {
341
+ res.writeHead(400, { "Content-Type": "application/json" });
342
+ res.end(JSON.stringify({ ok: false, error: "name required" }));
343
+ return;
344
+ }
345
+ if (!deps.runRecipeFn) {
346
+ res.writeHead(503, { "Content-Type": "application/json" });
347
+ res.end(JSON.stringify({
348
+ ok: false,
349
+ error: "Recipe execution unavailable — requires --claude-driver subprocess",
350
+ }));
351
+ return;
352
+ }
353
+ const result = await deps.runRecipeFn(name, vars);
354
+ res.writeHead(result.ok ? 200 : 400, {
355
+ "Content-Type": "application/json",
356
+ });
357
+ res.end(JSON.stringify(result));
358
+ }
359
+ catch {
360
+ respondInvalidJson(res);
361
+ }
362
+ })();
363
+ return true;
364
+ }
365
+ if (parsedUrl.pathname === "/activation-metrics" && req.method === "GET") {
366
+ try {
367
+ const metrics = loadActivationMetrics();
368
+ const summary = computeActivationSummary(metrics);
369
+ res.writeHead(200, { "Content-Type": "application/json" });
370
+ res.end(JSON.stringify({ metrics, summary }));
371
+ }
372
+ catch (err) {
373
+ res.writeHead(500, { "Content-Type": "application/json" });
374
+ res.end(JSON.stringify({
375
+ error: err instanceof Error ? err.message : String(err),
376
+ }));
377
+ }
378
+ return true;
379
+ }
380
+ if (parsedUrl.pathname === "/runs" && req.method === "GET") {
381
+ try {
382
+ const sp = parsedUrl.searchParams;
383
+ const limitRaw = sp.get("limit");
384
+ const afterRaw = sp.get("after");
385
+ const trigger = sp.get("trigger");
386
+ const status = sp.get("status");
387
+ const recipe = sp.get("recipe");
388
+ const limit = limitRaw ? Number.parseInt(limitRaw, 10) : Number.NaN;
389
+ const after = afterRaw ? Number.parseInt(afterRaw, 10) : Number.NaN;
390
+ const runs = deps.runsFn?.({
391
+ ...(Number.isFinite(limit) && { limit }),
392
+ ...(trigger && { trigger }),
393
+ ...(status && { status }),
394
+ ...(recipe && { recipe }),
395
+ ...(Number.isFinite(after) && { after }),
396
+ }) ?? [];
397
+ res.writeHead(200, { "Content-Type": "application/json" });
398
+ res.end(JSON.stringify({ runs }));
399
+ }
400
+ catch (err) {
401
+ res.writeHead(500, { "Content-Type": "application/json" });
402
+ res.end(JSON.stringify({
403
+ error: err instanceof Error ? err.message : String(err),
404
+ }));
405
+ }
406
+ return true;
407
+ }
408
+ // GET /runs/:seq — single run detail (includes stepResults if present)
409
+ const runDetailMatch = req.method === "GET" ? /^\/runs\/(\d+)$/.exec(parsedUrl.pathname) : null;
410
+ if (runDetailMatch?.[1]) {
411
+ const seq = Number.parseInt(runDetailMatch[1], 10);
412
+ try {
413
+ const run = deps.runDetailFn?.(seq) ?? null;
414
+ if (!run) {
415
+ res.writeHead(404, { "Content-Type": "application/json" });
416
+ res.end(JSON.stringify({ error: "not_found" }));
417
+ }
418
+ else {
419
+ res.writeHead(200, { "Content-Type": "application/json" });
420
+ res.end(JSON.stringify({ run }));
421
+ }
422
+ }
423
+ catch (err) {
424
+ res.writeHead(500, { "Content-Type": "application/json" });
425
+ res.end(JSON.stringify({
426
+ error: err instanceof Error ? err.message : String(err),
427
+ }));
428
+ }
429
+ return true;
430
+ }
431
+ // POST /runs/:seq/replay — VD-4 mocked replay. Re-runs the recipe with all
432
+ // tool/agent execution intercepted to return captured outputs from the
433
+ // original run. No external IO, no side effects. Real-mode replay is not
434
+ // exposed here yet — must ship separately with confirmation UX +
435
+ // kill-switch interaction.
436
+ const runReplayMatch = req.method === "POST"
437
+ ? /^\/runs\/(\d+)\/replay$/.exec(parsedUrl.pathname)
438
+ : null;
439
+ if (runReplayMatch?.[1]) {
440
+ const seq = Number.parseInt(runReplayMatch[1], 10);
441
+ void (async () => {
442
+ try {
443
+ if (!deps.runReplayFn) {
444
+ res.writeHead(503, { "Content-Type": "application/json" });
445
+ res.end(JSON.stringify({ error: "replay_unavailable" }));
446
+ return;
447
+ }
448
+ const result = await deps.runReplayFn(seq);
449
+ if (result.error === "run_not_found") {
450
+ res.writeHead(404, { "Content-Type": "application/json" });
451
+ }
452
+ else if (!result.ok) {
453
+ res.writeHead(500, { "Content-Type": "application/json" });
454
+ }
455
+ else {
456
+ res.writeHead(200, { "Content-Type": "application/json" });
457
+ }
458
+ res.end(JSON.stringify(result));
459
+ }
460
+ catch (err) {
461
+ const msg = err instanceof Error ? err.message : String(err);
462
+ res.writeHead(500, { "Content-Type": "application/json" });
463
+ res.end(JSON.stringify({ error: msg }));
464
+ }
465
+ })();
466
+ return true;
467
+ }
468
+ // GET /runs/:seq/plan — dry-run plan for the recipe that produced this run
469
+ const runPlanMatch = req.method === "GET"
470
+ ? /^\/runs\/(\d+)\/plan$/.exec(parsedUrl.pathname)
471
+ : null;
472
+ if (runPlanMatch?.[1]) {
473
+ const seq = Number.parseInt(runPlanMatch[1], 10);
474
+ void (async () => {
475
+ try {
476
+ const run = deps.runDetailFn?.(seq) ?? null;
477
+ if (!run) {
478
+ res.writeHead(404, { "Content-Type": "application/json" });
479
+ res.end(JSON.stringify({ error: "run_not_found" }));
480
+ return;
481
+ }
482
+ if (!deps.runPlanFn) {
483
+ res.writeHead(503, { "Content-Type": "application/json" });
484
+ res.end(JSON.stringify({ error: "plan_unavailable" }));
485
+ return;
486
+ }
487
+ // triggerSource appends ":agent" suffix — strip before file lookup
488
+ const recipeName = run.recipeName.replace(/:agent$/, "");
489
+ const plan = await deps.runPlanFn(recipeName);
490
+ res.writeHead(200, { "Content-Type": "application/json" });
491
+ res.end(JSON.stringify({ plan }));
492
+ }
493
+ catch (err) {
494
+ const msg = err instanceof Error ? err.message : String(err);
495
+ const status = msg.includes("not found") || msg.includes("ENOENT") ? 404 : 500;
496
+ res.writeHead(status, { "Content-Type": "application/json" });
497
+ res.end(JSON.stringify({ error: msg }));
498
+ }
499
+ })();
500
+ return true;
501
+ }
502
+ if (parsedUrl.pathname === "/recipes/generate" && req.method === "POST") {
503
+ void (async () => {
504
+ // Refill + try-consume one token. 429 if bucket is empty — `Retry-After`
505
+ // in seconds rounds up to the next refill of one whole token.
506
+ const now = Date.now();
507
+ const refilled = refillBucket(recipeGenerateBucket, now, RECIPE_GENERATE_LIMIT_PER_MIN);
508
+ const consumed = consumeToken(refilled);
509
+ recipeGenerateBucket = consumed.nextState;
510
+ if (!consumed.allowed) {
511
+ const secondsToOneToken = Math.ceil(((1 - consumed.nextState.tokens) / RECIPE_GENERATE_LIMIT_PER_MIN) *
512
+ 60);
513
+ res.writeHead(429, {
514
+ "Content-Type": "application/json",
515
+ "Retry-After": String(Math.max(1, secondsToOneToken)),
516
+ });
517
+ res.end(JSON.stringify({
518
+ ok: false,
519
+ error: `Rate limit exceeded — max ${RECIPE_GENERATE_LIMIT_PER_MIN} requests per minute`,
520
+ retryAfterSeconds: Math.max(1, secondsToOneToken),
521
+ }));
522
+ return;
523
+ }
524
+ const parsedBody = await readJsonBody(req, RECIPE_ROUTE_BODY_CAPS.generate);
525
+ if (!parsedBody.ok) {
526
+ if (parsedBody.code === "too_large") {
527
+ respond413(res, RECIPE_ROUTE_BODY_CAPS.generate);
528
+ }
529
+ else {
530
+ respondInvalidJson(res);
531
+ }
532
+ return;
533
+ }
534
+ const prompt = parsedBody.value
535
+ ?.prompt;
536
+ if (typeof prompt !== "string" || !prompt.trim()) {
537
+ res.writeHead(400, { "Content-Type": "application/json" });
538
+ res.end(JSON.stringify({
539
+ ok: false,
540
+ error: "prompt must be a non-empty string",
541
+ }));
542
+ return;
543
+ }
544
+ if (!deps.generateRecipeFn) {
545
+ res.writeHead(503, { "Content-Type": "application/json" });
546
+ res.end(JSON.stringify({
547
+ ok: false,
548
+ error: "Recipe generation unavailable — requires --claude-driver subprocess",
549
+ unavailable: true,
550
+ }));
551
+ return;
552
+ }
553
+ try {
554
+ const result = await deps.generateRecipeFn(prompt.trim());
555
+ const status = result.ok ? 200 : result.unavailable ? 503 : 422;
556
+ res.writeHead(status, { "Content-Type": "application/json" });
557
+ res.end(JSON.stringify(result));
558
+ }
559
+ catch (err) {
560
+ res.writeHead(500, { "Content-Type": "application/json" });
561
+ res.end(JSON.stringify({
562
+ ok: false,
563
+ error: err instanceof Error ? err.message : String(err),
564
+ }));
565
+ }
566
+ })();
567
+ return true;
568
+ }
569
+ if (req.url === "/recipes" && req.method === "POST") {
570
+ // A-PR2: bounded JSON read at RECIPE_ROUTE_BODY_CAPS.content (256 KB).
571
+ void (async () => {
572
+ const parsedBody = await readJsonBody(req, RECIPE_ROUTE_BODY_CAPS.content);
573
+ if (!parsedBody.ok) {
574
+ if (parsedBody.code === "too_large") {
575
+ respond413(res, RECIPE_ROUTE_BODY_CAPS.content);
576
+ }
577
+ else {
578
+ respondInvalidJson(res);
579
+ }
580
+ return;
581
+ }
582
+ try {
583
+ const draft = (parsedBody.value ?? {});
584
+ if (typeof draft.name !== "string" || !draft.name) {
585
+ res.writeHead(400, { "Content-Type": "application/json" });
586
+ res.end(JSON.stringify({ ok: false, error: "name required" }));
587
+ return;
588
+ }
589
+ if (!deps.saveRecipeFn) {
590
+ res.writeHead(503, { "Content-Type": "application/json" });
591
+ res.end(JSON.stringify({
592
+ ok: false,
593
+ error: "Recipe saving unavailable",
594
+ }));
595
+ return;
596
+ }
597
+ const result = deps.saveRecipeFn(draft);
598
+ res.writeHead(result.ok ? 201 : 400, {
599
+ "Content-Type": "application/json",
600
+ });
601
+ res.end(JSON.stringify(result));
602
+ }
603
+ catch {
604
+ respondInvalidJson(res);
605
+ }
606
+ })();
607
+ return true;
608
+ }
609
+ // PATCH /recipes/:name/trust — update trust level for a recipe.
610
+ const recipeTrustMatch = req.method === "PATCH"
611
+ ? /^\/recipes\/([^/]+)\/trust$/.exec(parsedUrl.pathname)
612
+ : null;
613
+ if (recipeTrustMatch?.[1]) {
614
+ const name = decodeURIComponent(recipeTrustMatch[1]);
615
+ void (async () => {
616
+ const parsedBody = await readJsonBody(req, RECIPE_ROUTE_BODY_CAPS.install);
617
+ if (!parsedBody.ok) {
618
+ if (parsedBody.code === "too_large") {
619
+ respond413(res, RECIPE_ROUTE_BODY_CAPS.install);
620
+ }
621
+ else {
622
+ respondInvalidJson(res);
623
+ }
624
+ return;
625
+ }
626
+ const level = parsedBody.value
627
+ ?.level;
628
+ if (typeof level !== "string" || !level) {
629
+ res.writeHead(400, { "Content-Type": "application/json" });
630
+ res.end(JSON.stringify({ ok: false, error: "level (string) required" }));
631
+ return;
632
+ }
633
+ if (!deps.setRecipeTrustFn) {
634
+ res.writeHead(503, { "Content-Type": "application/json" });
635
+ res.end(JSON.stringify({ ok: false, error: "Trust management unavailable" }));
636
+ return;
637
+ }
638
+ const result = deps.setRecipeTrustFn(name, level);
639
+ res.writeHead(result.ok ? 200 : 400, {
640
+ "Content-Type": "application/json",
641
+ });
642
+ res.end(JSON.stringify(result));
643
+ })();
644
+ return true;
645
+ }
646
+ const recipePatchMatch = /^\/recipes\/([^/]+)$/.exec(parsedUrl.pathname);
647
+ if (recipePatchMatch && req.method === "PATCH") {
648
+ // A-PR2: bounded JSON read at RECIPE_ROUTE_BODY_CAPS.content (256 KB).
649
+ const name = decodeURIComponent(recipePatchMatch[1] ?? "");
650
+ void (async () => {
651
+ const parsedBody = await readJsonBody(req, RECIPE_ROUTE_BODY_CAPS.content);
652
+ if (!parsedBody.ok) {
653
+ if (parsedBody.code === "too_large") {
654
+ respond413(res, RECIPE_ROUTE_BODY_CAPS.content);
655
+ }
656
+ else {
657
+ respondInvalidJson(res);
658
+ }
659
+ return;
660
+ }
661
+ try {
662
+ const body = parsedBody.value ?? {};
663
+ if (typeof body.enabled !== "boolean") {
664
+ res.writeHead(400, { "Content-Type": "application/json" });
665
+ res.end(JSON.stringify({
666
+ ok: false,
667
+ error: "enabled (boolean) required",
668
+ }));
669
+ return;
670
+ }
671
+ if (!deps.setRecipeEnabledFn) {
672
+ res.writeHead(503, { "Content-Type": "application/json" });
673
+ res.end(JSON.stringify({ ok: false, error: "Not available" }));
674
+ return;
675
+ }
676
+ const result = deps.setRecipeEnabledFn(name, body.enabled);
677
+ res.writeHead(result.ok ? 200 : 400, {
678
+ "Content-Type": "application/json",
679
+ });
680
+ res.end(JSON.stringify(result));
681
+ }
682
+ catch {
683
+ respondInvalidJson(res);
684
+ }
685
+ })();
686
+ return true;
687
+ }
688
+ if (parsedUrl.pathname === "/recipes/lint" && req.method === "POST") {
689
+ // A-PR2: bounded JSON read at RECIPE_ROUTE_BODY_CAPS.content (256 KB).
690
+ void (async () => {
691
+ const parsedBody = await readJsonBody(req, RECIPE_ROUTE_BODY_CAPS.content);
692
+ if (!parsedBody.ok) {
693
+ if (parsedBody.code === "too_large") {
694
+ respond413(res, RECIPE_ROUTE_BODY_CAPS.content);
695
+ }
696
+ else {
697
+ respondInvalidJson(res);
698
+ }
699
+ return;
700
+ }
701
+ try {
702
+ const body = parsedBody.value ?? {};
703
+ if (typeof body?.content !== "string") {
704
+ res.writeHead(400, { "Content-Type": "application/json" });
705
+ res.end(JSON.stringify({
706
+ ok: false,
707
+ error: "content (string) required",
708
+ }));
709
+ return;
710
+ }
711
+ if (!deps.lintRecipeContentFn) {
712
+ res.writeHead(503, { "Content-Type": "application/json" });
713
+ res.end(JSON.stringify({
714
+ ok: false,
715
+ error: "Recipe lint unavailable",
716
+ }));
717
+ return;
718
+ }
719
+ const result = deps.lintRecipeContentFn(body.content);
720
+ res.writeHead(200, { "Content-Type": "application/json" });
721
+ res.end(JSON.stringify(result));
722
+ }
723
+ catch {
724
+ respondInvalidJson(res);
725
+ }
726
+ })();
727
+ return true;
728
+ }
729
+ // GET /recipes/:name/plan — dry-run plan for a recipe by name. Returns the
730
+ // same RecipeDryRunPlan shape as GET /runs/:seq/plan but without needing a
731
+ // past run seq — useful for pre-flight review before a first run.
732
+ const recipePlanMatch = req.method === "GET"
733
+ ? /^\/recipes\/([^/]+)\/plan$/.exec(parsedUrl.pathname)
734
+ : null;
735
+ if (recipePlanMatch?.[1]) {
736
+ const name = decodeURIComponent(recipePlanMatch[1]);
737
+ void (async () => {
738
+ try {
739
+ if (!deps.runPlanFn) {
740
+ res.writeHead(503, { "Content-Type": "application/json" });
741
+ res.end(JSON.stringify({ error: "plan_unavailable" }));
742
+ return;
743
+ }
744
+ const plan = await deps.runPlanFn(name);
745
+ res.writeHead(200, { "Content-Type": "application/json" });
746
+ res.end(JSON.stringify({ plan }));
747
+ }
748
+ catch (err) {
749
+ const msg = err instanceof Error ? err.message : String(err);
750
+ const status = msg.includes("not found") || msg.includes("ENOENT") ? 404 : 500;
751
+ res.writeHead(status, { "Content-Type": "application/json" });
752
+ res.end(JSON.stringify({ error: msg }));
753
+ }
754
+ })();
755
+ return true;
756
+ }
757
+ const recipeContentMatch = /^\/recipes\/([^/]+)$/.exec(parsedUrl.pathname);
758
+ if (recipeContentMatch && req.method === "GET") {
759
+ const name = decodeURIComponent(recipeContentMatch[1] ?? "");
760
+ if (!deps.loadRecipeContentFn) {
761
+ res.writeHead(503, { "Content-Type": "application/json" });
762
+ res.end(JSON.stringify({ ok: false, error: "Recipe content unavailable" }));
763
+ return true;
764
+ }
765
+ const result = deps.loadRecipeContentFn(name);
766
+ if (!result) {
767
+ res.writeHead(404, { "Content-Type": "application/json" });
768
+ res.end(JSON.stringify({ ok: false, error: "Recipe not found" }));
769
+ return true;
770
+ }
771
+ res.writeHead(200, { "Content-Type": "application/json" });
772
+ res.end(JSON.stringify(result));
773
+ return true;
774
+ }
775
+ if (recipeContentMatch && req.method === "PUT") {
776
+ // A-PR2: bounded JSON read at RECIPE_ROUTE_BODY_CAPS.content (256 KB).
777
+ const name = decodeURIComponent(recipeContentMatch[1] ?? "");
778
+ void (async () => {
779
+ const parsedBody = await readJsonBody(req, RECIPE_ROUTE_BODY_CAPS.content);
780
+ if (!parsedBody.ok) {
781
+ if (parsedBody.code === "too_large") {
782
+ respond413(res, RECIPE_ROUTE_BODY_CAPS.content);
783
+ }
784
+ else {
785
+ respondInvalidJson(res);
786
+ }
787
+ return;
788
+ }
789
+ try {
790
+ const body = parsedBody.value ?? {};
791
+ if (typeof body.content !== "string") {
792
+ res.writeHead(400, { "Content-Type": "application/json" });
793
+ res.end(JSON.stringify({
794
+ ok: false,
795
+ error: "content (string) required",
796
+ }));
797
+ return;
798
+ }
799
+ if (!deps.saveRecipeContentFn) {
800
+ res.writeHead(503, { "Content-Type": "application/json" });
801
+ res.end(JSON.stringify({
802
+ ok: false,
803
+ error: "Recipe content saving unavailable",
804
+ }));
805
+ return;
806
+ }
807
+ const result = deps.saveRecipeContentFn(name, body.content);
808
+ res.writeHead(result.ok ? 200 : 400, {
809
+ "Content-Type": "application/json",
810
+ });
811
+ res.end(JSON.stringify(result));
812
+ }
813
+ catch {
814
+ respondInvalidJson(res);
815
+ }
816
+ })();
817
+ return true;
818
+ }
819
+ if (recipeContentMatch && req.method === "DELETE") {
820
+ const name = decodeURIComponent(recipeContentMatch[1] ?? "");
821
+ if (!deps.deleteRecipeContentFn) {
822
+ res.writeHead(503, { "Content-Type": "application/json" });
823
+ res.end(JSON.stringify({
824
+ ok: false,
825
+ error: "Recipe deletion unavailable",
826
+ }));
827
+ return true;
828
+ }
829
+ const result = deps.deleteRecipeContentFn(name);
830
+ const status = result.ok
831
+ ? 200
832
+ : result.error === "Recipe not found"
833
+ ? 404
834
+ : 400;
835
+ res.writeHead(status, { "Content-Type": "application/json" });
836
+ res.end(JSON.stringify(result));
837
+ return true;
838
+ }
839
+ // POST /recipes/:name/duplicate — copy recipe as next available variant name
840
+ const duplicateMatch = /^\/recipes\/([^/]+)\/duplicate$/.exec(parsedUrl.pathname);
841
+ if (duplicateMatch && req.method === "POST") {
842
+ const name = decodeURIComponent(duplicateMatch[1] ?? "");
843
+ if (!deps.duplicateRecipeFn) {
844
+ res.writeHead(503, { "Content-Type": "application/json" });
845
+ res.end(JSON.stringify({ ok: false, error: "Duplicate unavailable" }));
846
+ return true;
847
+ }
848
+ const result = deps.duplicateRecipeFn(name);
849
+ const status = result.ok
850
+ ? 201
851
+ : result.error === "Recipe not found"
852
+ ? 404
853
+ : 400;
854
+ res.writeHead(status, { "Content-Type": "application/json" });
855
+ res.end(JSON.stringify(result));
856
+ return true;
857
+ }
858
+ // POST /recipes/:name/promote — promote variant to canonical name.
859
+ // Body: { targetName: string }
860
+ const promoteMatch = /^\/recipes\/([^/]+)\/promote$/.exec(parsedUrl.pathname);
861
+ if (promoteMatch && req.method === "POST") {
862
+ const variantName = decodeURIComponent(promoteMatch[1] ?? "");
863
+ void (async () => {
864
+ const parsedBody = await readJsonBody(req, RECIPE_ROUTE_BODY_CAPS.content);
865
+ if (!parsedBody.ok) {
866
+ respondInvalidJson(res);
867
+ return;
868
+ }
869
+ const { targetName, force } = parsedBody.value ?? {};
870
+ if (typeof targetName !== "string" || !targetName.trim()) {
871
+ res.writeHead(400, { "Content-Type": "application/json" });
872
+ res.end(JSON.stringify({ ok: false, error: "targetName required" }));
873
+ return;
874
+ }
875
+ if (!deps.promoteRecipeVariantFn) {
876
+ res.writeHead(503, { "Content-Type": "application/json" });
877
+ res.end(JSON.stringify({ ok: false, error: "Promote unavailable" }));
878
+ return;
879
+ }
880
+ try {
881
+ const result = await deps.promoteRecipeVariantFn(variantName, targetName, {
882
+ force: force === true,
883
+ });
884
+ const httpStatus = result.ok
885
+ ? 200
886
+ : result.targetExists
887
+ ? 409
888
+ : result.error?.includes("not found")
889
+ ? 404
890
+ : 400;
891
+ res.writeHead(httpStatus, { "Content-Type": "application/json" });
892
+ res.end(JSON.stringify(result));
893
+ }
894
+ catch (err) {
895
+ res.writeHead(500, { "Content-Type": "application/json" });
896
+ res.end(JSON.stringify({
897
+ ok: false,
898
+ error: err instanceof Error ? err.message : String(err),
899
+ }));
900
+ }
901
+ })();
902
+ return true;
903
+ }
904
+ if (req.url === "/recipes" && req.method === "GET") {
905
+ try {
906
+ const data = deps.recipesFn?.() ?? { recipesDir: null, recipes: [] };
907
+ res.writeHead(200, { "Content-Type": "application/json" });
908
+ res.end(JSON.stringify(data));
909
+ }
910
+ catch (err) {
911
+ res.writeHead(500, { "Content-Type": "application/json" });
912
+ res.end(JSON.stringify({
913
+ error: err instanceof Error ? err.message : String(err),
914
+ }));
915
+ }
916
+ return true;
917
+ }
918
+ if (parsedUrl.pathname === "/templates" && req.method === "GET") {
919
+ void (async () => {
920
+ try {
921
+ const now = Date.now();
922
+ if (!templatesCache || now - templatesCacheTs > 5 * 60 * 1000) {
923
+ const ghRes = await fetch("https://raw.githubusercontent.com/patchworkos/recipes/main/index.json");
924
+ if (!ghRes.ok) {
925
+ throw new Error(`GitHub returned ${ghRes.status}`);
926
+ }
927
+ templatesCache = (await ghRes.json());
928
+ templatesCacheTs = now;
929
+ }
930
+ res.writeHead(200, { "Content-Type": "application/json" });
931
+ res.end(JSON.stringify(templatesCache));
932
+ }
933
+ catch (err) {
934
+ res.writeHead(502, { "Content-Type": "application/json" });
935
+ res.end(JSON.stringify({
936
+ ok: false,
937
+ error: err instanceof Error ? err.message : String(err),
938
+ }));
939
+ }
940
+ })();
941
+ return true;
942
+ }
943
+ if (parsedUrl.pathname === "/recipes/install" && req.method === "POST") {
944
+ // ---------------------------------------------------------------------
945
+ // BEGIN A-PR2 EDIT BLOCK — `/recipes/install` rework.
946
+ //
947
+ // Replaces the previous let-body-string accumulator with `readJsonBody`
948
+ // (4 KB cap), default-denies non-github sources via
949
+ // `CLAUDE_IDE_BRIDGE_INSTALL_ALLOWED_HOSTS`, and translates fetch errors
950
+ // into proper 4xx status codes (R2 H-routes Bug 2 — was always 500).
951
+ //
952
+ // SSRF guard runs AFTER allowlist match per R3 DP-2 sub-issue: this means
953
+ // an explicitly-allowlisted hostname STILL has to clear the SSRF check
954
+ // (so an admin can't accidentally allowlist `localhost`).
955
+ // ---------------------------------------------------------------------
956
+ void (async () => {
957
+ const parsedBody = await readJsonBody(req, RECIPE_ROUTE_BODY_CAPS.install);
958
+ if (!parsedBody.ok) {
959
+ if (parsedBody.code === "too_large") {
960
+ respond413(res, RECIPE_ROUTE_BODY_CAPS.install);
961
+ }
962
+ else {
963
+ respondInvalidJson(res);
964
+ }
965
+ return;
966
+ }
967
+ try {
968
+ const source = parsedBody.value?.source;
969
+ if (!source || typeof source !== "string") {
970
+ res.writeHead(400, { "Content-Type": "application/json" });
971
+ res.end(JSON.stringify({ ok: false, error: "Missing source field" }));
972
+ return;
973
+ }
974
+ // -----------------------------------------------------------------
975
+ // BUNDLE INSTALL DISPATCH (#130 PR A).
976
+ //
977
+ // `github:patchworkos/recipes/bundles/<name>` installs every recipe
978
+ // listed in the bundle's `patchwork-bundle.json`. Plugin (`plugin`)
979
+ // and policy template (`policy_template`) declared in the manifest
980
+ // are surfaced as advisory-only — wiring those needs separate
981
+ // decisions (npm-install surface, policy application UX) tracked
982
+ // outside this PR. See the #130 scoping comment.
983
+ // -----------------------------------------------------------------
984
+ const bundlePrefix = "github:patchworkos/recipes/bundles/";
985
+ if (source.startsWith(bundlePrefix)) {
986
+ const bundleName = source.slice(bundlePrefix.length);
987
+ const { isSafeBasename } = await import("./commands/recipeInstall.js");
988
+ if (!isSafeBasename(bundleName)) {
989
+ res.writeHead(400, { "Content-Type": "application/json" });
990
+ res.end(JSON.stringify({
991
+ ok: false,
992
+ error: "Invalid bundle name in source",
993
+ code: "invalid_bundle_name",
994
+ }));
995
+ return;
996
+ }
997
+ const manifestUrl = `https://raw.githubusercontent.com/patchworkos/recipes/main/bundles/${bundleName}/patchwork-bundle.json`;
998
+ const ctl = new AbortController();
999
+ const timeout = setTimeout(() => ctl.abort(), 30_000);
1000
+ let manifestRes;
1001
+ try {
1002
+ manifestRes = await fetch(manifestUrl, {
1003
+ signal: ctl.signal,
1004
+ redirect: "follow",
1005
+ });
1006
+ }
1007
+ catch (err) {
1008
+ clearTimeout(timeout);
1009
+ res.writeHead(502, { "Content-Type": "application/json" });
1010
+ res.end(JSON.stringify({
1011
+ ok: false,
1012
+ error: `Bundle manifest fetch failed: ${err instanceof Error ? err.message : String(err)}`,
1013
+ code: "bundle_fetch_network_error",
1014
+ }));
1015
+ return;
1016
+ }
1017
+ clearTimeout(timeout);
1018
+ if (!manifestRes.ok) {
1019
+ const outStatus = manifestRes.status === 404 ? 404 : 502;
1020
+ res.writeHead(outStatus, { "Content-Type": "application/json" });
1021
+ res.end(JSON.stringify({
1022
+ ok: false,
1023
+ error: `Bundle manifest at ${manifestUrl} returned ${manifestRes.status}`,
1024
+ code: "bundle_fetch_upstream_error",
1025
+ upstreamStatus: manifestRes.status,
1026
+ }));
1027
+ return;
1028
+ }
1029
+ // 64 KB hard cap on manifest body — real `patchwork-bundle.json`
1030
+ // is single-digit KB; anything past 64 KB is hostile or malformed.
1031
+ const manifestBuf = await manifestRes.arrayBuffer();
1032
+ if (manifestBuf.byteLength > 64 * 1024) {
1033
+ res.writeHead(413, { "Content-Type": "application/json" });
1034
+ res.end(JSON.stringify({
1035
+ ok: false,
1036
+ error: "Bundle manifest exceeds 64 KB cap",
1037
+ code: "bundle_manifest_too_large",
1038
+ }));
1039
+ return;
1040
+ }
1041
+ let manifest;
1042
+ try {
1043
+ manifest = JSON.parse(Buffer.from(manifestBuf).toString("utf-8"));
1044
+ }
1045
+ catch (err) {
1046
+ res.writeHead(502, { "Content-Type": "application/json" });
1047
+ res.end(JSON.stringify({
1048
+ ok: false,
1049
+ error: `Bundle manifest is not valid JSON: ${err instanceof Error ? err.message : String(err)}`,
1050
+ code: "bundle_manifest_invalid_json",
1051
+ }));
1052
+ return;
1053
+ }
1054
+ if (!Array.isArray(manifest.recipes) ||
1055
+ manifest.recipes.length === 0 ||
1056
+ !manifest.recipes.every((r) => typeof r === "string" && isSafeBasename(r))) {
1057
+ res.writeHead(400, { "Content-Type": "application/json" });
1058
+ res.end(JSON.stringify({
1059
+ ok: false,
1060
+ error: "Bundle manifest must declare a non-empty `recipes` array of safe recipe names",
1061
+ code: "bundle_manifest_invalid_recipes",
1062
+ }));
1063
+ return;
1064
+ }
1065
+ // Install each declared recipe. Errors are collected but don't
1066
+ // abort the loop — partial bundle install is more useful than
1067
+ // all-or-nothing when one of N recipes is broken.
1068
+ const installed = [];
1069
+ const failures = [];
1070
+ const { writeFileSync, mkdirSync, unlinkSync } = await import("node:fs");
1071
+ const { installRecipeFromFile } = await import("./recipes/installer.js");
1072
+ const recipesDir = path.join(os.homedir(), ".patchwork", "recipes");
1073
+ mkdirSync(recipesDir, { recursive: true });
1074
+ for (const r of manifest.recipes) {
1075
+ const recipeUrl = `https://raw.githubusercontent.com/patchworkos/recipes/main/recipes/${r}/${r}.yaml`;
1076
+ const recipeCtl = new AbortController();
1077
+ const recipeTimeout = setTimeout(() => recipeCtl.abort(), 30_000);
1078
+ try {
1079
+ const recipeRes = await fetch(recipeUrl, {
1080
+ signal: recipeCtl.signal,
1081
+ redirect: "follow",
1082
+ });
1083
+ clearTimeout(recipeTimeout);
1084
+ if (!recipeRes.ok) {
1085
+ failures.push({
1086
+ name: r,
1087
+ error: `Upstream returned ${recipeRes.status}`,
1088
+ });
1089
+ continue;
1090
+ }
1091
+ const recipeBuf = await recipeRes.arrayBuffer();
1092
+ if (recipeBuf.byteLength > 1024 * 1024) {
1093
+ failures.push({
1094
+ name: r,
1095
+ error: "Recipe body exceeded 1 MB cap",
1096
+ });
1097
+ continue;
1098
+ }
1099
+ const yamlText = Buffer.from(recipeBuf).toString("utf-8");
1100
+ const tmpFile = path.join(os.tmpdir(), `patchwork-bundle-install-${Date.now()}-${r}.yaml`);
1101
+ writeFileSync(tmpFile, yamlText, "utf-8");
1102
+ try {
1103
+ const installResult = installRecipeFromFile(tmpFile, {
1104
+ recipesDir,
1105
+ });
1106
+ installed.push({ name: r, action: installResult.action });
1107
+ }
1108
+ finally {
1109
+ try {
1110
+ unlinkSync(tmpFile);
1111
+ }
1112
+ catch {
1113
+ // best-effort cleanup
1114
+ }
1115
+ }
1116
+ }
1117
+ catch (err) {
1118
+ clearTimeout(recipeTimeout);
1119
+ failures.push({
1120
+ name: r,
1121
+ error: err instanceof Error ? err.message : String(err),
1122
+ });
1123
+ }
1124
+ }
1125
+ // Plugin / policy_template surfaced advisory-only.
1126
+ const advisory = {};
1127
+ if (typeof manifest.plugin === "string") {
1128
+ advisory.plugin = `Bundle declares plugin "${manifest.plugin}" — not installed; run \`npm install -g ${manifest.plugin}\` separately.`;
1129
+ }
1130
+ if (typeof manifest.policy_template === "string") {
1131
+ advisory.policy_template = `Bundle declares policy template "${manifest.policy_template}" — not applied; review and apply manually.`;
1132
+ }
1133
+ // 200 if any recipe installed; 502 otherwise. Always include both
1134
+ // arrays so callers (CLI + dashboard) can render partial-success.
1135
+ const status = installed.length > 0 ? 200 : 502;
1136
+ res.writeHead(status, { "Content-Type": "application/json" });
1137
+ res.end(JSON.stringify({
1138
+ ok: installed.length > 0,
1139
+ kind: "bundle",
1140
+ bundleName,
1141
+ installed,
1142
+ failures,
1143
+ ...(Object.keys(advisory).length > 0 && { advisory }),
1144
+ }));
1145
+ return;
1146
+ }
1147
+ const githubPrefix = "github:patchworkos/recipes/recipes/";
1148
+ let fetchUrl;
1149
+ let recipeName;
1150
+ if (source.startsWith(githubPrefix)) {
1151
+ recipeName = source.slice(githubPrefix.length);
1152
+ // The constructed URL is internal — recipeName must be a safe
1153
+ // single-segment so we don't end up encoding `../etc/passwd` into
1154
+ // the path. Reuse the strict basename predicate from `recipeInstall`.
1155
+ const { isSafeBasename } = await import("./commands/recipeInstall.js");
1156
+ if (!isSafeBasename(recipeName)) {
1157
+ res.writeHead(400, { "Content-Type": "application/json" });
1158
+ res.end(JSON.stringify({
1159
+ ok: false,
1160
+ error: "Invalid recipe name in source",
1161
+ }));
1162
+ return;
1163
+ }
1164
+ fetchUrl = `https://raw.githubusercontent.com/patchworkos/recipes/main/recipes/${recipeName}/${recipeName}.yaml`;
1165
+ }
1166
+ else if (source.startsWith("https://")) {
1167
+ // Non-github source: must clear the env-var allowlist AND the SSRF
1168
+ // guard. Default-deny when env var unset (R3 DP-2 confirmed).
1169
+ let parsedSource;
1170
+ try {
1171
+ parsedSource = new URL(source);
1172
+ }
1173
+ catch {
1174
+ res.writeHead(400, { "Content-Type": "application/json" });
1175
+ res.end(JSON.stringify({
1176
+ ok: false,
1177
+ error: "Invalid source URL",
1178
+ code: "invalid_source_url",
1179
+ }));
1180
+ return;
1181
+ }
1182
+ // Built-in github/raw.githubusercontent hosts are always permitted
1183
+ // — they match the github: shorthand surface above.
1184
+ const ALWAYS_ALLOWED = new Set([
1185
+ "github.com",
1186
+ "www.github.com",
1187
+ "raw.githubusercontent.com",
1188
+ ]);
1189
+ const envAllowed = (process.env.CLAUDE_IDE_BRIDGE_INSTALL_ALLOWED_HOSTS ?? "")
1190
+ .split(",")
1191
+ .map((h) => h.trim().toLowerCase())
1192
+ .filter(Boolean);
1193
+ const hostLower = parsedSource.hostname.toLowerCase();
1194
+ const inAllowlist = ALWAYS_ALLOWED.has(hostLower) || envAllowed.includes(hostLower);
1195
+ if (!inAllowlist) {
1196
+ res.writeHead(403, { "Content-Type": "application/json" });
1197
+ res.end(JSON.stringify({
1198
+ ok: false,
1199
+ error: `Host "${parsedSource.hostname}" is not in the install allowlist. Set CLAUDE_IDE_BRIDGE_INSTALL_ALLOWED_HOSTS to opt in.`,
1200
+ code: "host_not_allowlisted",
1201
+ }));
1202
+ return;
1203
+ }
1204
+ // SSRF guard runs AFTER allowlist — defends against operator-misuse
1205
+ // (allowlisting localhost or an internal mirror).
1206
+ const ssrf = await validateSafeUrl(source);
1207
+ if (!ssrf.ok) {
1208
+ res.writeHead(403, { "Content-Type": "application/json" });
1209
+ res.end(JSON.stringify({
1210
+ ok: false,
1211
+ error: `Host blocked by SSRF guard: ${ssrf.detail ?? ssrf.reason ?? "unknown"}`,
1212
+ code: "ssrf_blocked",
1213
+ }));
1214
+ return;
1215
+ }
1216
+ fetchUrl = source;
1217
+ const urlParts = fetchUrl.split("/");
1218
+ recipeName = (urlParts[urlParts.length - 1] ?? "recipe").replace(/\.ya?ml$/i, "");
1219
+ }
1220
+ else {
1221
+ res.writeHead(400, { "Content-Type": "application/json" });
1222
+ res.end(JSON.stringify({
1223
+ ok: false,
1224
+ error: "Unsupported source format",
1225
+ code: "unsupported_source",
1226
+ }));
1227
+ return;
1228
+ }
1229
+ // Bounded fetch — 1 MB hard cap on the response body so a malicious
1230
+ // host can't pin the install request open with a 1 GB stream.
1231
+ const fetchCtl = new AbortController();
1232
+ const fetchTimeout = setTimeout(() => fetchCtl.abort(), 30_000);
1233
+ let yamlRes;
1234
+ try {
1235
+ yamlRes = await fetch(fetchUrl, {
1236
+ signal: fetchCtl.signal,
1237
+ redirect: "follow",
1238
+ });
1239
+ }
1240
+ catch (err) {
1241
+ clearTimeout(fetchTimeout);
1242
+ // Network-level error → 502 (upstream unreachable), not 500.
1243
+ res.writeHead(502, { "Content-Type": "application/json" });
1244
+ res.end(JSON.stringify({
1245
+ ok: false,
1246
+ error: `Fetch failed: ${err instanceof Error ? err.message : String(err)}`,
1247
+ code: "fetch_network_error",
1248
+ }));
1249
+ return;
1250
+ }
1251
+ clearTimeout(fetchTimeout);
1252
+ if (!yamlRes.ok) {
1253
+ // Translate upstream HTTP into proper status — 404→404, 403→403,
1254
+ // 5xx→502 (don't leak the upstream 500 as our 500). R2 H-routes Bug 2.
1255
+ let outStatus = 502;
1256
+ if (yamlRes.status === 404)
1257
+ outStatus = 404;
1258
+ else if (yamlRes.status === 403)
1259
+ outStatus = 403;
1260
+ else if (yamlRes.status >= 400 && yamlRes.status < 500)
1261
+ outStatus = 400;
1262
+ res.writeHead(outStatus, { "Content-Type": "application/json" });
1263
+ res.end(JSON.stringify({
1264
+ ok: false,
1265
+ error: `Upstream returned ${yamlRes.status} ${yamlRes.statusText}`,
1266
+ code: "fetch_upstream_error",
1267
+ upstreamStatus: yamlRes.status,
1268
+ }));
1269
+ return;
1270
+ }
1271
+ // Streamed read with 1 MB cap (mirrors `httpClient` pattern).
1272
+ const MAX_RECIPE_BYTES = 1024 * 1024;
1273
+ const reader = yamlRes.body?.getReader();
1274
+ const chunks = [];
1275
+ let totalBytes = 0;
1276
+ let truncated = false;
1277
+ if (reader) {
1278
+ try {
1279
+ while (true) {
1280
+ const { done, value } = await reader.read();
1281
+ if (done || value === undefined)
1282
+ break;
1283
+ if (totalBytes + value.byteLength > MAX_RECIPE_BYTES) {
1284
+ truncated = true;
1285
+ await reader.cancel();
1286
+ break;
1287
+ }
1288
+ chunks.push(value);
1289
+ totalBytes += value.byteLength;
1290
+ }
1291
+ }
1292
+ finally {
1293
+ reader.releaseLock();
1294
+ }
1295
+ }
1296
+ if (truncated) {
1297
+ res.writeHead(413, { "Content-Type": "application/json" });
1298
+ res.end(JSON.stringify({
1299
+ ok: false,
1300
+ error: `Recipe body exceeded ${MAX_RECIPE_BYTES}-byte limit`,
1301
+ code: "recipe_too_large",
1302
+ }));
1303
+ return;
1304
+ }
1305
+ const yamlText = Buffer.concat(chunks.map((c) => Buffer.from(c))).toString("utf-8");
1306
+ const tmpFile = path.join(os.tmpdir(), `patchwork-install-${Date.now()}-${recipeName}.yaml`);
1307
+ const { writeFileSync, mkdirSync, unlinkSync } = await import("node:fs");
1308
+ writeFileSync(tmpFile, yamlText, "utf-8");
1309
+ let result;
1310
+ try {
1311
+ const recipesDir = path.join(os.homedir(), ".patchwork", "recipes");
1312
+ mkdirSync(recipesDir, { recursive: true });
1313
+ const { installRecipeFromFile } = await import("./recipes/installer.js");
1314
+ const installResult = installRecipeFromFile(tmpFile, {
1315
+ recipesDir,
1316
+ });
1317
+ result = { action: installResult.action, name: recipeName };
1318
+ }
1319
+ finally {
1320
+ try {
1321
+ unlinkSync(tmpFile);
1322
+ }
1323
+ catch {
1324
+ // best-effort cleanup
1325
+ }
1326
+ }
1327
+ res.writeHead(200, { "Content-Type": "application/json" });
1328
+ res.end(JSON.stringify({ ok: true, ...result }));
1329
+ }
1330
+ catch (err) {
1331
+ // Truly unexpected — installer crash, manifest validation throw, etc.
1332
+ res.writeHead(500, { "Content-Type": "application/json" });
1333
+ res.end(JSON.stringify({
1334
+ ok: false,
1335
+ error: err instanceof Error ? err.message : String(err),
1336
+ code: "install_internal_error",
1337
+ }));
1338
+ }
1339
+ })();
1340
+ // END A-PR2 EDIT BLOCK
1341
+ return true;
1342
+ }
1343
+ return false;
1344
+ }
1345
+ //# sourceMappingURL=recipeRoutes.js.map