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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (618) hide show
  1. package/README.bridge.md +6 -0
  2. package/README.md +318 -35
  3. package/deploy/bootstrap-new-vps.sh +12 -12
  4. package/deploy/bootstrap-vps.sh +187 -0
  5. package/deploy/deploy-dashboard.sh +174 -0
  6. package/deploy/deploy-landing.sh +136 -0
  7. package/dist/activationMetrics.d.ts +67 -0
  8. package/dist/activationMetrics.js +255 -0
  9. package/dist/activationMetrics.js.map +1 -0
  10. package/dist/activityLog.d.ts +49 -0
  11. package/dist/activityLog.js +78 -0
  12. package/dist/activityLog.js.map +1 -1
  13. package/dist/analyticsAggregator.d.ts +5 -1
  14. package/dist/analyticsAggregator.js +15 -4
  15. package/dist/analyticsAggregator.js.map +1 -1
  16. package/dist/analyticsPrefs.d.ts +11 -0
  17. package/dist/analyticsPrefs.js +33 -0
  18. package/dist/analyticsPrefs.js.map +1 -1
  19. package/dist/approvalHttp.d.ts +49 -2
  20. package/dist/approvalHttp.js +217 -21
  21. package/dist/approvalHttp.js.map +1 -1
  22. package/dist/approvalInsights.d.ts +49 -0
  23. package/dist/approvalInsights.js +97 -0
  24. package/dist/approvalInsights.js.map +1 -0
  25. package/dist/approvalQueue.d.ts +27 -1
  26. package/dist/approvalQueue.js +123 -3
  27. package/dist/approvalQueue.js.map +1 -1
  28. package/dist/approvalSignals.d.ts +124 -0
  29. package/dist/approvalSignals.js +512 -0
  30. package/dist/approvalSignals.js.map +1 -0
  31. package/dist/automation.d.ts +57 -0
  32. package/dist/automation.js +156 -59
  33. package/dist/automation.js.map +1 -1
  34. package/dist/automationSuggestions.d.ts +79 -0
  35. package/dist/automationSuggestions.js +150 -0
  36. package/dist/automationSuggestions.js.map +1 -0
  37. package/dist/bridge.d.ts +3 -0
  38. package/dist/bridge.js +194 -153
  39. package/dist/bridge.js.map +1 -1
  40. package/dist/bridgeToken.js +57 -19
  41. package/dist/bridgeToken.js.map +1 -1
  42. package/dist/ccPermissions.d.ts +15 -0
  43. package/dist/ccPermissions.js +21 -4
  44. package/dist/ccPermissions.js.map +1 -1
  45. package/dist/claudeDriver.d.ts +0 -16
  46. package/dist/claudeDriver.js +93 -36
  47. package/dist/claudeDriver.js.map +1 -1
  48. package/dist/claudeMdPatch.d.ts +9 -3
  49. package/dist/claudeMdPatch.js +79 -13
  50. package/dist/claudeMdPatch.js.map +1 -1
  51. package/dist/claudeOrchestrator.d.ts +13 -1
  52. package/dist/claudeOrchestrator.js +16 -8
  53. package/dist/claudeOrchestrator.js.map +1 -1
  54. package/dist/commands/dashboard.js +1 -1
  55. package/dist/commands/dashboard.js.map +1 -1
  56. package/dist/commands/launchd.d.ts +2 -0
  57. package/dist/commands/launchd.js +94 -0
  58. package/dist/commands/launchd.js.map +1 -0
  59. package/dist/commands/marketplace.d.ts +15 -10
  60. package/dist/commands/marketplace.js +27 -115
  61. package/dist/commands/marketplace.js.map +1 -1
  62. package/dist/commands/patchworkInit.d.ts +8 -0
  63. package/dist/commands/patchworkInit.js +77 -11
  64. package/dist/commands/patchworkInit.js.map +1 -1
  65. package/dist/commands/recipe.d.ts +289 -0
  66. package/dist/commands/recipe.js +1359 -0
  67. package/dist/commands/recipe.js.map +1 -0
  68. package/dist/commands/recipeInstall.d.ts +150 -0
  69. package/dist/commands/recipeInstall.js +647 -0
  70. package/dist/commands/recipeInstall.js.map +1 -0
  71. package/dist/commands/tracesExport.d.ts +83 -0
  72. package/dist/commands/tracesExport.js +269 -0
  73. package/dist/commands/tracesExport.js.map +1 -0
  74. package/dist/commands/tracesImport.d.ts +56 -0
  75. package/dist/commands/tracesImport.js +161 -0
  76. package/dist/commands/tracesImport.js.map +1 -0
  77. package/dist/commitIssueLinkLog.d.ts +8 -0
  78. package/dist/commitIssueLinkLog.js +53 -1
  79. package/dist/commitIssueLinkLog.js.map +1 -1
  80. package/dist/config.d.ts +23 -2
  81. package/dist/config.js +119 -9
  82. package/dist/config.js.map +1 -1
  83. package/dist/connectorRoutes.d.ts +43 -0
  84. package/dist/connectorRoutes.js +1300 -0
  85. package/dist/connectorRoutes.js.map +1 -0
  86. package/dist/connectors/asana.d.ts +198 -0
  87. package/dist/connectors/asana.js +679 -0
  88. package/dist/connectors/asana.js.map +1 -0
  89. package/dist/connectors/baseConnector.d.ts +153 -0
  90. package/dist/connectors/baseConnector.js +336 -0
  91. package/dist/connectors/baseConnector.js.map +1 -0
  92. package/dist/connectors/confluence.d.ts +111 -0
  93. package/dist/connectors/confluence.js +406 -0
  94. package/dist/connectors/confluence.js.map +1 -0
  95. package/dist/connectors/datadog.d.ts +116 -0
  96. package/dist/connectors/datadog.js +385 -0
  97. package/dist/connectors/datadog.js.map +1 -0
  98. package/dist/connectors/discord.d.ts +150 -0
  99. package/dist/connectors/discord.js +543 -0
  100. package/dist/connectors/discord.js.map +1 -0
  101. package/dist/connectors/fixtureLibrary.d.ts +21 -0
  102. package/dist/connectors/fixtureLibrary.js +70 -0
  103. package/dist/connectors/fixtureLibrary.js.map +1 -0
  104. package/dist/connectors/fixtureRecorder.d.ts +1 -0
  105. package/dist/connectors/fixtureRecorder.js +35 -0
  106. package/dist/connectors/fixtureRecorder.js.map +1 -0
  107. package/dist/connectors/github.js +17 -18
  108. package/dist/connectors/github.js.map +1 -1
  109. package/dist/connectors/gitlab.d.ts +180 -0
  110. package/dist/connectors/gitlab.js +582 -0
  111. package/dist/connectors/gitlab.js.map +1 -0
  112. package/dist/connectors/gmail.d.ts +4 -1
  113. package/dist/connectors/gmail.js +149 -27
  114. package/dist/connectors/gmail.js.map +1 -1
  115. package/dist/connectors/googleCalendar.d.ts +4 -1
  116. package/dist/connectors/googleCalendar.js +88 -25
  117. package/dist/connectors/googleCalendar.js.map +1 -1
  118. package/dist/connectors/googleDrive.d.ts +34 -0
  119. package/dist/connectors/googleDrive.js +321 -0
  120. package/dist/connectors/googleDrive.js.map +1 -0
  121. package/dist/connectors/htmlEscape.d.ts +5 -0
  122. package/dist/connectors/htmlEscape.js +13 -0
  123. package/dist/connectors/htmlEscape.js.map +1 -0
  124. package/dist/connectors/hubspot.d.ts +112 -0
  125. package/dist/connectors/hubspot.js +408 -0
  126. package/dist/connectors/hubspot.js.map +1 -0
  127. package/dist/connectors/intercom.d.ts +102 -0
  128. package/dist/connectors/intercom.js +402 -0
  129. package/dist/connectors/intercom.js.map +1 -0
  130. package/dist/connectors/jira.d.ts +98 -0
  131. package/dist/connectors/jira.js +396 -0
  132. package/dist/connectors/jira.js.map +1 -0
  133. package/dist/connectors/linear.js +30 -19
  134. package/dist/connectors/linear.js.map +1 -1
  135. package/dist/connectors/mcpOAuth.d.ts +3 -0
  136. package/dist/connectors/mcpOAuth.js +64 -10
  137. package/dist/connectors/mcpOAuth.js.map +1 -1
  138. package/dist/connectors/mockConnector.d.ts +28 -0
  139. package/dist/connectors/mockConnector.js +81 -0
  140. package/dist/connectors/mockConnector.js.map +1 -0
  141. package/dist/connectors/notion.d.ts +143 -0
  142. package/dist/connectors/notion.js +424 -0
  143. package/dist/connectors/notion.js.map +1 -0
  144. package/dist/connectors/oauthStateStore.d.ts +31 -0
  145. package/dist/connectors/oauthStateStore.js +52 -0
  146. package/dist/connectors/oauthStateStore.js.map +1 -0
  147. package/dist/connectors/pagerduty.d.ts +160 -0
  148. package/dist/connectors/pagerduty.js +464 -0
  149. package/dist/connectors/pagerduty.js.map +1 -0
  150. package/dist/connectors/sentry.js +5 -13
  151. package/dist/connectors/sentry.js.map +1 -1
  152. package/dist/connectors/slack.d.ts +16 -1
  153. package/dist/connectors/slack.js +155 -32
  154. package/dist/connectors/slack.js.map +1 -1
  155. package/dist/connectors/stripe.d.ts +116 -0
  156. package/dist/connectors/stripe.js +379 -0
  157. package/dist/connectors/stripe.js.map +1 -0
  158. package/dist/connectors/tokenStorage.d.ts +35 -0
  159. package/dist/connectors/tokenStorage.js +484 -0
  160. package/dist/connectors/tokenStorage.js.map +1 -0
  161. package/dist/connectors/zendesk.d.ts +104 -0
  162. package/dist/connectors/zendesk.js +442 -0
  163. package/dist/connectors/zendesk.js.map +1 -0
  164. package/dist/cors.d.ts +10 -0
  165. package/dist/cors.js +29 -0
  166. package/dist/cors.js.map +1 -0
  167. package/dist/decisionReplay.d.ts +72 -0
  168. package/dist/decisionReplay.js +92 -0
  169. package/dist/decisionReplay.js.map +1 -0
  170. package/dist/decisionTraceLog.d.ts +6 -0
  171. package/dist/decisionTraceLog.js +54 -2
  172. package/dist/decisionTraceLog.js.map +1 -1
  173. package/dist/drivers/claude/subprocess.d.ts +12 -2
  174. package/dist/drivers/claude/subprocess.js +79 -6
  175. package/dist/drivers/claude/subprocess.js.map +1 -1
  176. package/dist/drivers/gemini/api.d.ts +18 -0
  177. package/dist/drivers/gemini/api.js +29 -0
  178. package/dist/drivers/gemini/api.js.map +1 -0
  179. package/dist/drivers/gemini/index.d.ts +5 -1
  180. package/dist/drivers/gemini/index.js +39 -5
  181. package/dist/drivers/gemini/index.js.map +1 -1
  182. package/dist/drivers/index.d.ts +8 -1
  183. package/dist/drivers/index.js +10 -2
  184. package/dist/drivers/index.js.map +1 -1
  185. package/dist/drivers/local/index.d.ts +26 -0
  186. package/dist/drivers/local/index.js +41 -0
  187. package/dist/drivers/local/index.js.map +1 -0
  188. package/dist/featureFlags.d.ts +79 -0
  189. package/dist/featureFlags.js +208 -0
  190. package/dist/featureFlags.js.map +1 -0
  191. package/dist/fp/automationInterpreter.js +26 -21
  192. package/dist/fp/automationInterpreter.js.map +1 -1
  193. package/dist/fp/automationProgram.d.ts +1 -1
  194. package/dist/fp/automationProgram.js.map +1 -1
  195. package/dist/fp/automationState.js +4 -1
  196. package/dist/fp/automationState.js.map +1 -1
  197. package/dist/fp/policyParser.js +21 -1
  198. package/dist/fp/policyParser.js.map +1 -1
  199. package/dist/httpErrorResponse.d.ts +36 -0
  200. package/dist/httpErrorResponse.js +46 -0
  201. package/dist/httpErrorResponse.js.map +1 -0
  202. package/dist/inboxRoutes.d.ts +22 -0
  203. package/dist/inboxRoutes.js +193 -0
  204. package/dist/inboxRoutes.js.map +1 -0
  205. package/dist/index.d.ts +1 -1
  206. package/dist/index.js +1403 -203
  207. package/dist/index.js.map +1 -1
  208. package/dist/installGuard.d.ts +25 -0
  209. package/dist/installGuard.js +48 -0
  210. package/dist/installGuard.js.map +1 -0
  211. package/dist/mcpRoutes.d.ts +37 -0
  212. package/dist/mcpRoutes.js +76 -0
  213. package/dist/mcpRoutes.js.map +1 -0
  214. package/dist/oauth.d.ts +20 -1
  215. package/dist/oauth.js +214 -39
  216. package/dist/oauth.js.map +1 -1
  217. package/dist/oauthRoutes.d.ts +32 -0
  218. package/dist/oauthRoutes.js +119 -0
  219. package/dist/oauthRoutes.js.map +1 -0
  220. package/dist/orchestrator/orchestratorBridge.js +2 -2
  221. package/dist/orchestrator/orchestratorBridge.js.map +1 -1
  222. package/dist/patchworkConfig.d.ts +29 -0
  223. package/dist/patchworkConfig.js +100 -5
  224. package/dist/patchworkConfig.js.map +1 -1
  225. package/dist/pluginLoader.d.ts +28 -0
  226. package/dist/pluginLoader.js +77 -11
  227. package/dist/pluginLoader.js.map +1 -1
  228. package/dist/pluginWatcher.js +8 -3
  229. package/dist/pluginWatcher.js.map +1 -1
  230. package/dist/preToolUseHook.d.ts +12 -0
  231. package/dist/preToolUseHook.js +30 -1
  232. package/dist/preToolUseHook.js.map +1 -1
  233. package/dist/prompts.js +4 -0
  234. package/dist/prompts.js.map +1 -1
  235. package/dist/recipeOrchestration.d.ts +121 -0
  236. package/dist/recipeOrchestration.js +965 -0
  237. package/dist/recipeOrchestration.js.map +1 -0
  238. package/dist/recipeRoutes.d.ts +185 -0
  239. package/dist/recipeRoutes.js +1369 -0
  240. package/dist/recipeRoutes.js.map +1 -0
  241. package/dist/recipes/RecipeOrchestrator.d.ts +40 -0
  242. package/dist/recipes/RecipeOrchestrator.js +51 -0
  243. package/dist/recipes/RecipeOrchestrator.js.map +1 -0
  244. package/dist/recipes/agentExecutor.d.ts +38 -0
  245. package/dist/recipes/agentExecutor.js +50 -0
  246. package/dist/recipes/agentExecutor.js.map +1 -0
  247. package/dist/recipes/chainedRunner.d.ts +191 -0
  248. package/dist/recipes/chainedRunner.js +759 -0
  249. package/dist/recipes/chainedRunner.js.map +1 -0
  250. package/dist/recipes/compiler.js +3 -3
  251. package/dist/recipes/compiler.js.map +1 -1
  252. package/dist/recipes/dependencyGraph.d.ts +39 -0
  253. package/dist/recipes/dependencyGraph.js +199 -0
  254. package/dist/recipes/dependencyGraph.js.map +1 -0
  255. package/dist/recipes/disabledMarkers.d.ts +48 -0
  256. package/dist/recipes/disabledMarkers.js +52 -0
  257. package/dist/recipes/disabledMarkers.js.map +1 -0
  258. package/dist/recipes/installer.js +3 -3
  259. package/dist/recipes/installer.js.map +1 -1
  260. package/dist/recipes/legacyRecipeCompat.d.ts +10 -0
  261. package/dist/recipes/legacyRecipeCompat.js +131 -0
  262. package/dist/recipes/legacyRecipeCompat.js.map +1 -0
  263. package/dist/recipes/manifest.d.ts +47 -0
  264. package/dist/recipes/manifest.js +156 -0
  265. package/dist/recipes/manifest.js.map +1 -0
  266. package/dist/recipes/migrationWarnings.d.ts +12 -0
  267. package/dist/recipes/migrationWarnings.js +44 -0
  268. package/dist/recipes/migrationWarnings.js.map +1 -0
  269. package/dist/recipes/migrations/index.d.ts +24 -0
  270. package/dist/recipes/migrations/index.js +55 -0
  271. package/dist/recipes/migrations/index.js.map +1 -0
  272. package/dist/recipes/migrations/types.d.ts +28 -0
  273. package/dist/recipes/migrations/types.js +2 -0
  274. package/dist/recipes/migrations/types.js.map +1 -0
  275. package/dist/recipes/migrations/v1.d.ts +11 -0
  276. package/dist/recipes/migrations/v1.js +18 -0
  277. package/dist/recipes/migrations/v1.js.map +1 -0
  278. package/dist/recipes/names.d.ts +40 -0
  279. package/dist/recipes/names.js +66 -0
  280. package/dist/recipes/names.js.map +1 -0
  281. package/dist/recipes/nestedRecipeStep.d.ts +58 -0
  282. package/dist/recipes/nestedRecipeStep.js +95 -0
  283. package/dist/recipes/nestedRecipeStep.js.map +1 -0
  284. package/dist/recipes/outputRegistry.d.ts +28 -0
  285. package/dist/recipes/outputRegistry.js +52 -0
  286. package/dist/recipes/outputRegistry.js.map +1 -0
  287. package/dist/recipes/parser.js +4 -1
  288. package/dist/recipes/parser.js.map +1 -1
  289. package/dist/recipes/replayRun.d.ts +62 -0
  290. package/dist/recipes/replayRun.js +97 -0
  291. package/dist/recipes/replayRun.js.map +1 -0
  292. package/dist/recipes/resolveRecipePath.d.ts +69 -0
  293. package/dist/recipes/resolveRecipePath.js +202 -0
  294. package/dist/recipes/resolveRecipePath.js.map +1 -0
  295. package/dist/recipes/scheduler.d.ts +23 -7
  296. package/dist/recipes/scheduler.js +225 -45
  297. package/dist/recipes/scheduler.js.map +1 -1
  298. package/dist/recipes/schema.d.ts +17 -2
  299. package/dist/recipes/schemaGenerator.d.ts +28 -0
  300. package/dist/recipes/schemaGenerator.js +565 -0
  301. package/dist/recipes/schemaGenerator.js.map +1 -0
  302. package/dist/recipes/stepObservation.d.ts +44 -0
  303. package/dist/recipes/stepObservation.js +232 -0
  304. package/dist/recipes/stepObservation.js.map +1 -0
  305. package/dist/recipes/templateEngine.d.ts +62 -0
  306. package/dist/recipes/templateEngine.js +201 -0
  307. package/dist/recipes/templateEngine.js.map +1 -0
  308. package/dist/recipes/toolRegistry.d.ts +186 -0
  309. package/dist/recipes/toolRegistry.js +309 -0
  310. package/dist/recipes/toolRegistry.js.map +1 -0
  311. package/dist/recipes/tools/asana.d.ts +16 -0
  312. package/dist/recipes/tools/asana.js +524 -0
  313. package/dist/recipes/tools/asana.js.map +1 -0
  314. package/dist/recipes/tools/calendar.d.ts +6 -0
  315. package/dist/recipes/tools/calendar.js +61 -0
  316. package/dist/recipes/tools/calendar.js.map +1 -0
  317. package/dist/recipes/tools/confluence.d.ts +6 -0
  318. package/dist/recipes/tools/confluence.js +254 -0
  319. package/dist/recipes/tools/confluence.js.map +1 -0
  320. package/dist/recipes/tools/datadog.d.ts +6 -0
  321. package/dist/recipes/tools/datadog.js +239 -0
  322. package/dist/recipes/tools/datadog.js.map +1 -0
  323. package/dist/recipes/tools/diagnostics.d.ts +6 -0
  324. package/dist/recipes/tools/diagnostics.js +36 -0
  325. package/dist/recipes/tools/diagnostics.js.map +1 -0
  326. package/dist/recipes/tools/discord.d.ts +18 -0
  327. package/dist/recipes/tools/discord.js +254 -0
  328. package/dist/recipes/tools/discord.js.map +1 -0
  329. package/dist/recipes/tools/file.d.ts +12 -0
  330. package/dist/recipes/tools/file.js +174 -0
  331. package/dist/recipes/tools/file.js.map +1 -0
  332. package/dist/recipes/tools/git.d.ts +6 -0
  333. package/dist/recipes/tools/git.js +63 -0
  334. package/dist/recipes/tools/git.js.map +1 -0
  335. package/dist/recipes/tools/github.d.ts +6 -0
  336. package/dist/recipes/tools/github.js +116 -0
  337. package/dist/recipes/tools/github.js.map +1 -0
  338. package/dist/recipes/tools/gitlab.d.ts +11 -0
  339. package/dist/recipes/tools/gitlab.js +285 -0
  340. package/dist/recipes/tools/gitlab.js.map +1 -0
  341. package/dist/recipes/tools/gmail.d.ts +6 -0
  342. package/dist/recipes/tools/gmail.js +451 -0
  343. package/dist/recipes/tools/gmail.js.map +1 -0
  344. package/dist/recipes/tools/googleDrive.d.ts +1 -0
  345. package/dist/recipes/tools/googleDrive.js +55 -0
  346. package/dist/recipes/tools/googleDrive.js.map +1 -0
  347. package/dist/recipes/tools/hubspot.d.ts +6 -0
  348. package/dist/recipes/tools/hubspot.js +232 -0
  349. package/dist/recipes/tools/hubspot.js.map +1 -0
  350. package/dist/recipes/tools/index.d.ts +30 -0
  351. package/dist/recipes/tools/index.js +33 -0
  352. package/dist/recipes/tools/index.js.map +1 -0
  353. package/dist/recipes/tools/intercom.d.ts +6 -0
  354. package/dist/recipes/tools/intercom.js +226 -0
  355. package/dist/recipes/tools/intercom.js.map +1 -0
  356. package/dist/recipes/tools/jira.d.ts +14 -0
  357. package/dist/recipes/tools/jira.js +369 -0
  358. package/dist/recipes/tools/jira.js.map +1 -0
  359. package/dist/recipes/tools/linear.d.ts +7 -0
  360. package/dist/recipes/tools/linear.js +307 -0
  361. package/dist/recipes/tools/linear.js.map +1 -0
  362. package/dist/recipes/tools/meetingNotes.d.ts +21 -0
  363. package/dist/recipes/tools/meetingNotes.js +701 -0
  364. package/dist/recipes/tools/meetingNotes.js.map +1 -0
  365. package/dist/recipes/tools/notion.d.ts +6 -0
  366. package/dist/recipes/tools/notion.js +278 -0
  367. package/dist/recipes/tools/notion.js.map +1 -0
  368. package/dist/recipes/tools/pagerduty.d.ts +15 -0
  369. package/dist/recipes/tools/pagerduty.js +451 -0
  370. package/dist/recipes/tools/pagerduty.js.map +1 -0
  371. package/dist/recipes/tools/sentry.d.ts +12 -0
  372. package/dist/recipes/tools/sentry.js +73 -0
  373. package/dist/recipes/tools/sentry.js.map +1 -0
  374. package/dist/recipes/tools/slack.d.ts +6 -0
  375. package/dist/recipes/tools/slack.js +82 -0
  376. package/dist/recipes/tools/slack.js.map +1 -0
  377. package/dist/recipes/tools/stripe.d.ts +6 -0
  378. package/dist/recipes/tools/stripe.js +265 -0
  379. package/dist/recipes/tools/stripe.js.map +1 -0
  380. package/dist/recipes/tools/zendesk.d.ts +6 -0
  381. package/dist/recipes/tools/zendesk.js +245 -0
  382. package/dist/recipes/tools/zendesk.js.map +1 -0
  383. package/dist/recipes/validation.d.ts +13 -0
  384. package/dist/recipes/validation.js +617 -0
  385. package/dist/recipes/validation.js.map +1 -0
  386. package/dist/recipes/yamlRunner.d.ts +130 -2
  387. package/dist/recipes/yamlRunner.js +1009 -402
  388. package/dist/recipes/yamlRunner.js.map +1 -1
  389. package/dist/recipesHttp.d.ts +151 -6
  390. package/dist/recipesHttp.js +999 -29
  391. package/dist/recipesHttp.js.map +1 -1
  392. package/dist/riskTier.js +7 -1
  393. package/dist/riskTier.js.map +1 -1
  394. package/dist/runLog.d.ts +100 -1
  395. package/dist/runLog.js +258 -5
  396. package/dist/runLog.js.map +1 -1
  397. package/dist/schemas/dry-run-plan.v1.json +139 -0
  398. package/dist/schemas/recipe.v1.json +684 -0
  399. package/dist/server.d.ts +127 -8
  400. package/dist/server.js +740 -933
  401. package/dist/server.js.map +1 -1
  402. package/dist/ssrfGuard.d.ts +54 -0
  403. package/dist/ssrfGuard.js +122 -0
  404. package/dist/ssrfGuard.js.map +1 -0
  405. package/dist/streamableHttp.d.ts +39 -1
  406. package/dist/streamableHttp.js +128 -17
  407. package/dist/streamableHttp.js.map +1 -1
  408. package/dist/tokenUsageTracker.d.ts +33 -0
  409. package/dist/tokenUsageTracker.js +146 -0
  410. package/dist/tokenUsageTracker.js.map +1 -0
  411. package/dist/tools/activityLog.d.ts +2 -0
  412. package/dist/tools/addLinearComment.d.ts +1 -0
  413. package/dist/tools/addLinearComment.js +4 -2
  414. package/dist/tools/addLinearComment.js.map +1 -1
  415. package/dist/tools/batchLsp.d.ts +3 -0
  416. package/dist/tools/bridgeDoctor.d.ts +1 -0
  417. package/dist/tools/bridgeDoctor.js +2 -2
  418. package/dist/tools/bridgeDoctor.js.map +1 -1
  419. package/dist/tools/bridgeStatus.d.ts +1 -0
  420. package/dist/tools/cancelClaudeTask.d.ts +2 -0
  421. package/dist/tools/cancelClaudeTask.js +1 -0
  422. package/dist/tools/cancelClaudeTask.js.map +1 -1
  423. package/dist/tools/checkDocumentDirty.d.ts +1 -0
  424. package/dist/tools/clipboard.d.ts +2 -0
  425. package/dist/tools/closeTabs.d.ts +2 -0
  426. package/dist/tools/codeLens.d.ts +1 -0
  427. package/dist/tools/contextBundle.d.ts +1 -0
  428. package/dist/tools/createIssueFromAIComment.d.ts +1 -0
  429. package/dist/tools/createLinearIssue.d.ts +1 -0
  430. package/dist/tools/ctxGetTaskContext.d.ts +1 -0
  431. package/dist/tools/ctxQueryTraces.d.ts +1 -0
  432. package/dist/tools/ctxSaveTrace.d.ts +1 -0
  433. package/dist/tools/debug.d.ts +4 -0
  434. package/dist/tools/decorations.d.ts +2 -0
  435. package/dist/tools/documentLinks.d.ts +1 -0
  436. package/dist/tools/editText.d.ts +1 -0
  437. package/dist/tools/enrichCommit.d.ts +1 -0
  438. package/dist/tools/enrichStackTrace.d.ts +1 -0
  439. package/dist/tools/explainDiagnostic.d.ts +1 -0
  440. package/dist/tools/explainSymbol.d.ts +1 -0
  441. package/dist/tools/fetchCalendarEvents.d.ts +1 -0
  442. package/dist/tools/fetchGithubIssue.d.ts +1 -0
  443. package/dist/tools/fetchGithubPR.d.ts +1 -0
  444. package/dist/tools/fetchLinearIssue.d.ts +1 -0
  445. package/dist/tools/fetchSentryIssue.d.ts +1 -0
  446. package/dist/tools/fetchSlackProfile.d.ts +1 -0
  447. package/dist/tools/fetchSlackProfile.js +4 -1
  448. package/dist/tools/fetchSlackProfile.js.map +1 -1
  449. package/dist/tools/fileOperations.d.ts +3 -0
  450. package/dist/tools/fileWatcher.d.ts +2 -0
  451. package/dist/tools/findFiles.d.ts +1 -0
  452. package/dist/tools/findRelatedTests.d.ts +1 -0
  453. package/dist/tools/fixAllLintErrors.d.ts +1 -0
  454. package/dist/tools/foldingRanges.d.ts +1 -0
  455. package/dist/tools/formatDocument.d.ts +1 -0
  456. package/dist/tools/generateTests.d.ts +1 -0
  457. package/dist/tools/getAIComments.d.ts +1 -0
  458. package/dist/tools/getAnalyticsReport.d.ts +1 -0
  459. package/dist/tools/getArchitectureContext.d.ts +1 -0
  460. package/dist/tools/getBufferContent.d.ts +1 -0
  461. package/dist/tools/getChangeImpact.d.ts +1 -0
  462. package/dist/tools/getClaudeTaskStatus.d.ts +2 -0
  463. package/dist/tools/getClaudeTaskStatus.js +1 -0
  464. package/dist/tools/getClaudeTaskStatus.js.map +1 -1
  465. package/dist/tools/getCodeCoverage.d.ts +1 -0
  466. package/dist/tools/getCommitsForIssue.d.ts +1 -0
  467. package/dist/tools/getConnectorStatus.d.ts +1 -0
  468. package/dist/tools/getCurrentSelection.d.ts +2 -0
  469. package/dist/tools/getDebugState.d.ts +1 -0
  470. package/dist/tools/getDependencyTree.d.ts +1 -0
  471. package/dist/tools/getDiagnostics.d.ts +1 -0
  472. package/dist/tools/getDiffFromHandoff.d.ts +1 -0
  473. package/dist/tools/getDocumentSymbols.d.ts +25 -0
  474. package/dist/tools/getDocumentSymbols.js +74 -8
  475. package/dist/tools/getDocumentSymbols.js.map +1 -1
  476. package/dist/tools/getFileTree.d.ts +1 -0
  477. package/dist/tools/getGitDiff.d.ts +1 -0
  478. package/dist/tools/getGitHotspots.d.ts +1 -0
  479. package/dist/tools/getGitLog.d.ts +1 -0
  480. package/dist/tools/getGitStatus.d.ts +1 -0
  481. package/dist/tools/getImportTree.d.ts +1 -0
  482. package/dist/tools/getImportedSignatures.d.ts +1 -0
  483. package/dist/tools/getOpenEditors.d.ts +1 -0
  484. package/dist/tools/getPRTemplate.d.ts +1 -0
  485. package/dist/tools/getProjectContext.d.ts +1 -0
  486. package/dist/tools/getProjectInfo.d.ts +1 -0
  487. package/dist/tools/getSecurityAdvisories.d.ts +1 -0
  488. package/dist/tools/getSecurityAdvisories.js +10 -1
  489. package/dist/tools/getSecurityAdvisories.js.map +1 -1
  490. package/dist/tools/getSessionUsage.d.ts +4 -0
  491. package/dist/tools/getSessionUsage.js +3 -0
  492. package/dist/tools/getSessionUsage.js.map +1 -1
  493. package/dist/tools/getSymbolHistory.d.ts +1 -0
  494. package/dist/tools/getToolCapabilities.d.ts +1 -0
  495. package/dist/tools/getTypeSignature.d.ts +1 -0
  496. package/dist/tools/getWorkspaceFolders.d.ts +1 -0
  497. package/dist/tools/getWorkspaceSettings.d.ts +1 -0
  498. package/dist/tools/gitHistory.d.ts +2 -0
  499. package/dist/tools/gitWrite.d.ts +11 -0
  500. package/dist/tools/github/actions.d.ts +2 -0
  501. package/dist/tools/github/actions.js +4 -2
  502. package/dist/tools/github/actions.js.map +1 -1
  503. package/dist/tools/github/composite.d.ts +342 -0
  504. package/dist/tools/github/composite.js +343 -0
  505. package/dist/tools/github/composite.js.map +1 -0
  506. package/dist/tools/github/index.d.ts +1 -0
  507. package/dist/tools/github/index.js +1 -0
  508. package/dist/tools/github/index.js.map +1 -1
  509. package/dist/tools/github/issues.d.ts +4 -0
  510. package/dist/tools/github/issues.js +8 -4
  511. package/dist/tools/github/issues.js.map +1 -1
  512. package/dist/tools/github/pr.d.ts +7 -0
  513. package/dist/tools/github/pr.js +50 -12
  514. package/dist/tools/github/pr.js.map +1 -1
  515. package/dist/tools/handoffNote.d.ts +4 -0
  516. package/dist/tools/handoffNote.js +2 -0
  517. package/dist/tools/handoffNote.js.map +1 -1
  518. package/dist/tools/hoverAtCursor.d.ts +1 -0
  519. package/dist/tools/httpClient.d.ts +2 -0
  520. package/dist/tools/index.d.ts +8 -0
  521. package/dist/tools/index.js +47 -8
  522. package/dist/tools/index.js.map +1 -1
  523. package/dist/tools/inlayHints.d.ts +1 -0
  524. package/dist/tools/launchQuickTask.d.ts +2 -0
  525. package/dist/tools/launchQuickTask.js +1 -0
  526. package/dist/tools/launchQuickTask.js.map +1 -1
  527. package/dist/tools/listClaudeTasks.d.ts +2 -0
  528. package/dist/tools/listClaudeTasks.js +1 -0
  529. package/dist/tools/listClaudeTasks.js.map +1 -1
  530. package/dist/tools/listTerminals.d.ts +1 -0
  531. package/dist/tools/lsp.d.ts +14 -0
  532. package/dist/tools/navigateToSymbolByName.d.ts +1 -0
  533. package/dist/tools/openDiff.d.ts +1 -0
  534. package/dist/tools/openFile.d.ts +1 -0
  535. package/dist/tools/openInBrowser.d.ts +1 -0
  536. package/dist/tools/organizeImports.d.ts +1 -0
  537. package/dist/tools/performanceReport.d.ts +1 -0
  538. package/dist/tools/planPersistence.d.ts +5 -0
  539. package/dist/tools/previewEdit.d.ts +1 -0
  540. package/dist/tools/refactorAnalyze.d.ts +1 -0
  541. package/dist/tools/refactorPreview.d.ts +2 -0
  542. package/dist/tools/refactorPreview.js +1 -0
  543. package/dist/tools/refactorPreview.js.map +1 -1
  544. package/dist/tools/replaceBlock.d.ts +1 -0
  545. package/dist/tools/resumeClaudeTask.d.ts +2 -0
  546. package/dist/tools/resumeClaudeTask.js +1 -0
  547. package/dist/tools/resumeClaudeTask.js.map +1 -1
  548. package/dist/tools/runClaudeTask.d.ts +2 -0
  549. package/dist/tools/runClaudeTask.js +1 -0
  550. package/dist/tools/runClaudeTask.js.map +1 -1
  551. package/dist/tools/runCommand.d.ts +1 -0
  552. package/dist/tools/runCommand.js +5 -0
  553. package/dist/tools/runCommand.js.map +1 -1
  554. package/dist/tools/runTests.d.ts +1 -0
  555. package/dist/tools/saveDocument.d.ts +1 -0
  556. package/dist/tools/screenshotAndAnnotate.d.ts +1 -0
  557. package/dist/tools/searchAndReplace.d.ts +1 -0
  558. package/dist/tools/searchTools.d.ts +1 -0
  559. package/dist/tools/searchTools.js +1 -1
  560. package/dist/tools/searchTools.js.map +1 -1
  561. package/dist/tools/searchWorkspace.d.ts +1 -0
  562. package/dist/tools/selectionRanges.d.ts +1 -0
  563. package/dist/tools/semanticTokens.d.ts +1 -0
  564. package/dist/tools/setActiveWorkspaceFolder.d.ts +1 -0
  565. package/dist/tools/signatureHelp.d.ts +1 -0
  566. package/dist/tools/slackListChannels.d.ts +1 -0
  567. package/dist/tools/slackListChannels.js.map +1 -1
  568. package/dist/tools/slackPostMessage.d.ts +1 -0
  569. package/dist/tools/slackPostMessage.js +11 -6
  570. package/dist/tools/slackPostMessage.js.map +1 -1
  571. package/dist/tools/terminal.d.ts +6 -0
  572. package/dist/tools/terminal.js +4 -0
  573. package/dist/tools/terminal.js.map +1 -1
  574. package/dist/tools/testTraceToSource.d.ts +1 -0
  575. package/dist/tools/testTraceToSource.js +2 -2
  576. package/dist/tools/testTraceToSource.js.map +1 -1
  577. package/dist/tools/transaction.d.ts +23 -0
  578. package/dist/tools/transaction.js +29 -0
  579. package/dist/tools/transaction.js.map +1 -1
  580. package/dist/tools/typeHierarchy.d.ts +1 -0
  581. package/dist/tools/updateLinearIssue.d.ts +1 -0
  582. package/dist/tools/updateLinearIssue.js +20 -6
  583. package/dist/tools/updateLinearIssue.js.map +1 -1
  584. package/dist/tools/utils.d.ts +6 -0
  585. package/dist/tools/utils.js +59 -0
  586. package/dist/tools/utils.js.map +1 -1
  587. package/dist/tools/vscodeCommands.d.ts +2 -0
  588. package/dist/tools/vscodeTasks.d.ts +2 -0
  589. package/dist/tools/workspaceSettings.d.ts +1 -0
  590. package/dist/traceEncryption.d.ts +46 -0
  591. package/dist/traceEncryption.js +124 -0
  592. package/dist/traceEncryption.js.map +1 -0
  593. package/dist/transport.d.ts +46 -1
  594. package/dist/transport.js +173 -19
  595. package/dist/transport.js.map +1 -1
  596. package/package.json +30 -8
  597. package/scripts/mcp-stdio-shim.cjs +19 -3
  598. package/scripts/start-all.sh +34 -3
  599. package/templates/automation-policies/recipe-authoring.json +25 -0
  600. package/templates/automation-policy.example.json +6 -0
  601. package/templates/co.patchwork-os.bridge.plist +34 -0
  602. package/templates/policies/README.md +72 -0
  603. package/templates/policies/conservative.json +14 -0
  604. package/templates/policies/developer.json +14 -0
  605. package/templates/policies/headless-ci.json +24 -0
  606. package/templates/policies/personal-assistant.json +15 -0
  607. package/templates/policies/regulated-industry.json +18 -0
  608. package/templates/recipes/approval-queue-ui-test.yaml +205 -0
  609. package/templates/recipes/lint-on-save.yaml +1 -2
  610. package/templates/recipes/morning-brief-slack.yaml +57 -0
  611. package/templates/recipes/morning-brief.yaml +2 -2
  612. package/templates/recipes/project-health-check.yaml +50 -0
  613. package/templates/recipes/webhook/README.md +70 -0
  614. package/templates/recipes/webhook/capture-thought.yaml +26 -0
  615. package/templates/recipes/webhook/customer-escalation.yaml +49 -0
  616. package/templates/recipes/webhook/incident-intake.yaml +46 -0
  617. package/templates/recipes/webhook/meeting-prep.yaml +48 -0
  618. package/templates/recipes/webhook/morning-brief.yaml +57 -0
package/dist/index.js CHANGED
@@ -3,10 +3,15 @@
3
3
  // Uses Node 20.6+ native dotenv loader; falls back to manual parse for older Node.
4
4
  {
5
5
  const { fileURLToPath: _fileURLToPath } = await import("node:url");
6
- const envPath = _fileURLToPath(new URL("../.env", import.meta.url));
7
6
  try {
8
7
  const { readFileSync, existsSync } = await import("node:fs");
9
- if (existsSync(envPath)) {
8
+ // Try both "../.env" (compiled dist/) and ".env" (tsx src/ dev run)
9
+ const candidates = [
10
+ _fileURLToPath(new URL("../.env", import.meta.url)),
11
+ _fileURLToPath(new URL(".env", import.meta.url)),
12
+ ];
13
+ const envPath = candidates.find(existsSync);
14
+ if (envPath) {
10
15
  for (const line of readFileSync(envPath, "utf-8").split("\n")) {
11
16
  const m = /^([A-Z_][A-Z0-9_]*)=(.*)$/.exec(line.trim());
12
17
  if (m?.[1] && !process.env[m[1]]) {
@@ -35,14 +40,30 @@ import { getAnalyticsPref, setAnalyticsPref } from "./analyticsPrefs.js";
35
40
  import { Bridge } from "./bridge.js";
36
41
  import { isBridgeToolsFileValid, repairBridgeToolsRulesIfStale, } from "./bridgeToolsRules.js";
37
42
  import { findEditor, parseConfig } from "./config.js";
43
+ import { detectWorkspaceSymlinkInstall, PATCHWORK_PACKAGE_NAME, SYMLINK_INSTALL_FIX, } from "./installGuard.js";
38
44
  import { PACKAGE_VERSION, semverGt } from "./version.js";
39
45
  const __dirnameTop = path.dirname(fileURLToPath(import.meta.url));
46
+ // Warn when a symlinked global install is detected (`npm install -g .`).
47
+ // launchctl / sandbox environments can fail through that link with EPERM.
48
+ // Warn only — do not crash interactive or dev flows.
49
+ {
50
+ const _symlinkInfo = detectWorkspaceSymlinkInstall();
51
+ if (_symlinkInfo) {
52
+ process.stderr.write(`\n⚠️ Detected a symlinked global ${PATCHWORK_PACKAGE_NAME} install.\n` +
53
+ ` Logical root: ${_symlinkInfo.logicalRoot}\n` +
54
+ ` Real path: ${_symlinkInfo.realRoot}\n\n` +
55
+ " LaunchAgent startup can fail with EPERM when the macOS sandbox\n" +
56
+ " cannot access workspace files under ~/Documents through that link.\n\n" +
57
+ SYMLINK_INSTALL_FIX +
58
+ "\n");
59
+ }
60
+ }
40
61
  const OPEN_VSX_PUBLISHER = "oolab-labs";
41
62
  const OPEN_VSX_NAME = "claude-ide-bridge-extension";
42
63
  // CLAUDE.md versioned-block patching moved to ./claudeMdPatch.ts so tests
43
64
  // can import the helpers without triggering the top-level CLI side effects
44
65
  // at the bottom of this file. Re-exported here for back-compat.
45
- export { BRIDGE_BLOCK_END, BRIDGE_BLOCK_RE, bridgeBlockStartMarker, extractClaudeMdBlockVersion, patchClaudeMdImport, } from "./claudeMdPatch.js";
66
+ export { BRIDGE_BLOCK_END, bridgeBlockStartMarker, extractClaudeMdBlockVersion, patchClaudeMdImport, replaceAllBridgeBlocks, } from "./claudeMdPatch.js";
46
67
  import { extractClaudeMdBlockVersion, patchClaudeMdImport, } from "./claudeMdPatch.js";
47
68
  /**
48
69
  * Downloads the latest VSIX from Open VSX Registry to a temp file.
@@ -71,6 +92,62 @@ async function downloadVsixFromOpenVsx() {
71
92
  writeFileSync(tmpPath, Buffer.from(buf));
72
93
  return tmpPath;
73
94
  }
95
+ // Closes the race where bridge.start() began initialising in parallel with
96
+ // a subcommand's async work — observed in the 2026-04-29 dogfood pass
97
+ // where `recipe install` errors interleaved with bridge "Tools: full"
98
+ // startup logs.
99
+ //
100
+ // Every subcommand `if`-block below dispatches via an `(async () => {...})()`
101
+ // IIFE that ends with `process.exit`. The IIFE invocation returns
102
+ // synchronously, so without this gate, control immediately falls through
103
+ // to the bridge.start() block at end-of-file and starts initialising
104
+ // alongside the subcommand's async work. process.exit fires *eventually*
105
+ // after the await chain, but the bridge has already begun in parallel.
106
+ // Two IIFEs (patchwork no-args dashboard, recipe watch) lack process.exit
107
+ // entirely — without this gate they would run alongside the bridge
108
+ // indefinitely.
109
+ //
110
+ // Single source of truth for "is this argv invoking a subcommand?" — the
111
+ // same list is also used by the unknown-command suggester at L2570.
112
+ const KNOWN_SUBCOMMANDS = [
113
+ "init",
114
+ "patchwork-init",
115
+ "start-all",
116
+ "install-extension",
117
+ "gen-claude-md",
118
+ "print-token",
119
+ "gen-plugin-stub",
120
+ "notify",
121
+ "install",
122
+ "marketplace",
123
+ "status",
124
+ "shim",
125
+ "recipe",
126
+ "traces",
127
+ "suggest",
128
+ "dashboard",
129
+ "launchd",
130
+ "start",
131
+ ];
132
+ const __invokedSubcommand = (() => {
133
+ const sub = process.argv[2];
134
+ if (!sub || sub.startsWith("-"))
135
+ return null;
136
+ // Treat KNOWN_SUBCOMMANDS as the dispatch source. The bare-binary
137
+ // dashboard launcher (no argv) is handled separately below.
138
+ return KNOWN_SUBCOMMANDS.includes(sub)
139
+ ? sub
140
+ : null;
141
+ })();
142
+ const __invokedBareBinaryDashboard = (() => {
143
+ if (process.argv[2] && process.argv[2] !== "dashboard")
144
+ return false;
145
+ const binName = path.basename(process.argv[1] ?? "");
146
+ return (binName === "patchwork-os" ||
147
+ binName === "patchwork" ||
148
+ binName === "patchwork.js");
149
+ })();
150
+ const __subcommandWillRun = __invokedSubcommand !== null || __invokedBareBinaryDashboard;
74
151
  // Handle --version flag — print package version and exit.
75
152
  if (process.argv[2] === "--version" || process.argv[2] === "-v") {
76
153
  console.log(`claude-ide-bridge ${PACKAGE_VERSION}`);
@@ -97,6 +174,49 @@ if (isStartAll) {
97
174
  });
98
175
  process.exit(result.status ?? 1);
99
176
  }
177
+ // `patchwork start` — opinionated front door over start-all.
178
+ // Defaults to full mode (all tools registered) and the web dashboard, so the
179
+ // doc-promised "patchwork start → everything works" path actually works.
180
+ // Pass-through args still go to start-all.sh; --help short-circuits.
181
+ if (process.argv[2] === "start") {
182
+ const passthrough = process.argv.slice(3);
183
+ if (passthrough.includes("--help") || passthrough.includes("-h")) {
184
+ process.stdout.write(`patchwork start — Launch the full Patchwork stack
185
+
186
+ Starts bridge + Claude Code + dashboard in a tmux session.
187
+ Defaults to full mode so all bridge tools are registered.
188
+
189
+ Usage: patchwork start [options]
190
+
191
+ Options:
192
+ --workspace <path> Directory to open (default: current directory)
193
+ --no-dashboard Skip the web dashboard
194
+ --dashboard-port <N> Dashboard port (default: 3200)
195
+ --notify <topic> Push notifications via ntfy.sh
196
+ --vps <user@host> SSH reverse tunnel for stable claude.ai URL
197
+ --slim Slim mode (27 IDE-exclusive tools only — overrides default)
198
+ --help, -h Show this help
199
+
200
+ This is a thin wrapper over \`start-all\`. For advanced flags see:
201
+ patchwork start-all --help
202
+ `);
203
+ process.exit(0);
204
+ }
205
+ // Default to --full unless caller opted into slim explicitly.
206
+ const args = [...passthrough];
207
+ const slimIdx = args.indexOf("--slim");
208
+ if (slimIdx >= 0) {
209
+ args.splice(slimIdx, 1); // strip — start-all.sh has no --slim flag, slim is its default
210
+ }
211
+ else if (!args.includes("--full")) {
212
+ args.push("--full");
213
+ }
214
+ const scriptPath = path.resolve(__dirnameTop, "..", "scripts", "start-all.sh");
215
+ const result = spawnSync("bash", [scriptPath, ...args], {
216
+ stdio: "inherit",
217
+ });
218
+ process.exit(result.status ?? 1);
219
+ }
100
220
  function writeRulesFileAtomic(rulesFilePath, content) {
101
221
  const tmpPath = `${rulesFilePath}.tmp`;
102
222
  writeFileSync(tmpPath, content, { encoding: "utf-8", flag: "wx" });
@@ -248,7 +368,8 @@ if (process.argv[2] === "install") {
248
368
  await runInstall(process.argv.slice(3));
249
369
  process.exit(0);
250
370
  }
251
- // Handle marketplace subcommand — browse and install community skills
371
+ // Handle marketplace subcommand — DEPRECATED, prints migration message.
372
+ // See issue #279 and src/commands/marketplace.ts for the rationale.
252
373
  if (process.argv[2] === "marketplace") {
253
374
  const { runMarketplace } = await import("./commands/marketplace.js");
254
375
  await runMarketplace(process.argv.slice(3));
@@ -468,14 +589,20 @@ Options:
468
589
  // Handle gen-plugin-stub subcommand — scaffolds a new plugin directory
469
590
  if (process.argv[2] === "gen-plugin-stub") {
470
591
  const argv = process.argv.slice(3);
471
- // Parse args: gen-plugin-stub <dir> [--name <name>] [--prefix <prefix>]
592
+ // Parse args: gen-plugin-stub <dir> [--name <name>] [--prefix <prefix>] [--ts]
472
593
  const dirArg = argv.find((a) => !a.startsWith("--"));
473
594
  if (!dirArg) {
474
- process.stderr.write("Usage: claude-ide-bridge gen-plugin-stub <output-dir> [--name <org/plugin-name>] [--prefix <toolPrefix>]\n");
595
+ process.stderr.write("Usage: claude-ide-bridge gen-plugin-stub <output-dir> [--name <org/plugin-name>] [--prefix <toolPrefix>] [--ts]\n");
475
596
  process.exit(1);
476
597
  }
477
598
  const nameIdx = argv.indexOf("--name");
478
599
  const prefixIdx = argv.indexOf("--prefix");
600
+ // --ts emits a TypeScript variant (src/index.ts + tsconfig.json + build
601
+ // scripts) alongside a compiled-output manifest pointing at index.mjs.
602
+ // Plugin authors get type-checked tools without changing the hot-reload
603
+ // contract — `npm run dev` watches src/, emits index.mjs, bridge picks
604
+ // up the rebuilt artifact via --plugin-watch.
605
+ const useTypeScript = argv.includes("--ts");
479
606
  const pluginName = nameIdx !== -1 && argv[nameIdx + 1]
480
607
  ? argv[nameIdx + 1]
481
608
  : "my-org/my-plugin";
@@ -509,8 +636,8 @@ if (process.argv[2] === "gen-plugin-stub") {
509
636
  minBridgeVersion: "2.1.24",
510
637
  };
511
638
  writeFileSync(path.join(outDir, "claude-ide-bridge-plugin.json"), `${JSON.stringify(manifest, null, 2)}\n`, "utf-8");
512
- // index.mjs entrypoint
513
- const entrypoint = `/**
639
+ // ── shared tool body — same logic, different surface syntax ──
640
+ const jsEntrypoint = `/**
514
641
  * ${pluginName} — Claude IDE Bridge plugin
515
642
  *
516
643
  * Each tool must have a name starting with "${toolPrefix}".
@@ -518,7 +645,7 @@ if (process.argv[2] === "gen-plugin-stub") {
518
645
  * ctx.config (commandTimeout, maxResultSize), and ctx.logger.
519
646
  */
520
647
 
521
- /** @param {import('claude-ide-bridge/plugin').PluginContext} ctx */
648
+ /** @param {import('patchwork-os/plugin').PluginContext} ctx */
522
649
  export function register(ctx) {
523
650
  ctx.logger.info(${JSON.stringify(`${pluginName} loaded`)}, { workspace: ctx.workspace });
524
651
 
@@ -546,70 +673,381 @@ export function register(ctx) {
546
673
  };
547
674
  }
548
675
  `;
549
- writeFileSync(path.join(outDir, "index.mjs"), entrypoint, "utf-8");
550
- // package.json (optional, for npm publishing)
551
- const pkg = {
676
+ const tsEntrypoint = `/**
677
+ * ${pluginName} Claude IDE Bridge plugin
678
+ *
679
+ * Each tool must have a name starting with "${toolPrefix}".
680
+ * The \`ctx\` object provides: ctx.workspace, ctx.workspaceFolders,
681
+ * ctx.config (commandTimeout, maxResultSize), and ctx.logger.
682
+ */
683
+ import type { PluginContext } from "patchwork-os/plugin";
684
+
685
+ interface HelloArgs {
686
+ name: string;
687
+ }
688
+
689
+ export function register(ctx: PluginContext) {
690
+ ctx.logger.info(${JSON.stringify(`${pluginName} loaded`)}, { workspace: ctx.workspace });
691
+
692
+ return {
693
+ tools: [
694
+ {
695
+ schema: {
696
+ name: ${JSON.stringify(`${toolPrefix}Hello`)},
697
+ description: "Example tool — returns a greeting",
698
+ inputSchema: {
699
+ type: "object" as const,
700
+ required: ["name"] as const,
701
+ additionalProperties: false as const,
702
+ properties: {
703
+ name: { type: "string" as const, description: "Name to greet" },
704
+ },
705
+ },
706
+ annotations: { readOnlyHint: true },
707
+ },
708
+ handler: async (args: HelloArgs) => ({
709
+ content: [
710
+ {
711
+ type: "text" as const,
712
+ text: \`Hello from ${pluginName}, \${args.name}!\`,
713
+ },
714
+ ],
715
+ }),
716
+ },
717
+ ],
718
+ };
719
+ }
720
+ `;
721
+ // Write entrypoint — TS goes under src/, JS at root.
722
+ if (useTypeScript) {
723
+ mkdirSync(path.join(outDir, "src"), { recursive: true });
724
+ writeFileSync(path.join(outDir, "src", "index.ts"), tsEntrypoint, "utf-8");
725
+ }
726
+ else {
727
+ writeFileSync(path.join(outDir, "index.mjs"), jsEntrypoint, "utf-8");
728
+ }
729
+ // tsconfig.json — TS variant only. Emits a single ESM file at index.mjs
730
+ // so the plugin manifest's entrypoint stays the same shape as the JS
731
+ // scaffold and --plugin-watch reload semantics don't change.
732
+ if (useTypeScript) {
733
+ const tsconfig = {
734
+ compilerOptions: {
735
+ target: "ES2022",
736
+ module: "ES2022",
737
+ moduleResolution: "Bundler",
738
+ outDir: ".",
739
+ rootDir: "src",
740
+ declaration: false,
741
+ strict: true,
742
+ esModuleInterop: true,
743
+ skipLibCheck: true,
744
+ resolveJsonModule: true,
745
+ // Emit .mjs so the plugin loader (which expects ESM) picks it up
746
+ // without relying on package.json "type": "module" alone.
747
+ // tsc doesn't emit .mjs natively, so package.json's "build" script
748
+ // does a rename pass — see below.
749
+ },
750
+ include: ["src/**/*"],
751
+ exclude: ["node_modules"],
752
+ };
753
+ writeFileSync(path.join(outDir, "tsconfig.json"), `${JSON.stringify(tsconfig, null, 2)}\n`, "utf-8");
754
+ }
755
+ // package.json — TS variant adds build + dev (watch) scripts.
756
+ const pkgBase = {
552
757
  name: pluginName.replace(/^@[^/]+\//, "").replace(/\//g, "-"),
553
758
  version: "0.1.0",
554
759
  description: "A Claude IDE Bridge plugin",
555
760
  type: "module",
556
761
  main: "index.mjs",
557
- keywords: ["claude-ide-bridge", "claude-ide-bridge-plugin"],
558
- peerDependencies: { "claude-ide-bridge": ">=2.1.24" },
762
+ keywords: ["patchwork-os", "claude-ide-bridge-plugin"],
763
+ peerDependencies: { "patchwork-os": ">=0.2.0-alpha.0" },
559
764
  };
765
+ const pkg = useTypeScript
766
+ ? {
767
+ ...pkgBase,
768
+ scripts: {
769
+ // tsc emits index.js — rename to index.mjs so the loader treats
770
+ // it as ESM regardless of the consumer's package.json.
771
+ build: "tsc && mv index.js index.mjs",
772
+ dev: "tsc --watch",
773
+ clean: "rm -f index.mjs",
774
+ },
775
+ devDependencies: {
776
+ typescript: "^5.4.0",
777
+ "patchwork-os": ">=0.2.0-alpha.0",
778
+ },
779
+ }
780
+ : pkgBase;
560
781
  writeFileSync(path.join(outDir, "package.json"), `${JSON.stringify(pkg, null, 2)}\n`, "utf-8");
782
+ // README.md — included in both variants. Spells out the hot-reload
783
+ // contract so plugin authors don't have to read the platform docs to
784
+ // get started.
785
+ const readmeBody = useTypeScript
786
+ ? `# ${pluginName}
787
+
788
+ A [Claude IDE Bridge](https://github.com/Oolab-labs/patchwork-os) plugin (TypeScript).
789
+
790
+ ## Quick start
791
+
792
+ \`\`\`sh
793
+ npm install
794
+ npm run dev # in one terminal — watches src/, emits index.mjs
795
+
796
+ # In another terminal:
797
+ claude-ide-bridge --plugin . --plugin-watch
798
+ \`\`\`
799
+
800
+ Edit \`src/index.ts\`. \`tsc --watch\` rebuilds, the bridge hot-reloads, your tool is callable from the live Claude session on the next turn.
801
+
802
+ ## Build for distribution
803
+
804
+ \`\`\`sh
805
+ npm run build # emits index.mjs
806
+ npm publish # publish to npm (optional)
807
+ \`\`\`
808
+
809
+ When published with the \`claude-ide-bridge-plugin\` keyword, users can install with:
810
+
811
+ \`\`\`sh
812
+ claude-ide-bridge --plugin ${pluginName.replace(/^@[^/]+\//, "")}
813
+ \`\`\`
814
+
815
+ ## Tool naming
816
+
817
+ Every tool exposed by this plugin **must** have a \`name\` starting with \`${toolPrefix}\`. The bridge enforces this at load time (\`/^[a-zA-Z][a-zA-Z0-9_]{1,19}$/\`).
818
+
819
+ ## Plugin context
820
+
821
+ The \`ctx\` argument to \`register()\` provides:
822
+
823
+ - \`ctx.workspace\` — workspace root path
824
+ - \`ctx.workspaceFolders\` — array of workspace folders
825
+ - \`ctx.config\` — \`{ commandTimeout, maxResultSize }\`
826
+ - \`ctx.logger\` — \`info\` / \`warn\` / \`error\` logging that respects bridge log level
827
+
828
+ ## Live toolsmithing
829
+
830
+ The whole point of plugins is that you can author tools *while Claude is using the bridge*. Edit \`src/index.ts\`, save, the watcher rebuilds, the bridge reloads — Claude's next turn sees the new tool.
831
+
832
+ See [documents/live-toolsmithing.md](https://github.com/Oolab-labs/patchwork-os/blob/main/documents/live-toolsmithing.md) for the full narrative.
833
+ `
834
+ : `# ${pluginName}
835
+
836
+ A [Claude IDE Bridge](https://github.com/Oolab-labs/patchwork-os) plugin.
837
+
838
+ ## Quick start
839
+
840
+ \`\`\`sh
841
+ claude-ide-bridge --plugin . --plugin-watch
842
+ \`\`\`
843
+
844
+ Edit \`index.mjs\`. The bridge hot-reloads on save — your tool is callable from the live Claude session on the next turn. No build step needed for the JS variant.
845
+
846
+ ## Tool naming
847
+
848
+ Every tool exposed by this plugin **must** have a \`name\` starting with \`${toolPrefix}\`. The bridge enforces this at load time (\`/^[a-zA-Z][a-zA-Z0-9_]{1,19}$/\`).
849
+
850
+ ## Plugin context
851
+
852
+ The \`ctx\` argument to \`register()\` provides:
853
+
854
+ - \`ctx.workspace\` — workspace root path
855
+ - \`ctx.workspaceFolders\` — array of workspace folders
856
+ - \`ctx.config\` — \`{ commandTimeout, maxResultSize }\`
857
+ - \`ctx.logger\` — \`info\` / \`warn\` / \`error\` logging that respects bridge log level
858
+
859
+ ## Want types?
860
+
861
+ Re-scaffold with \`claude-ide-bridge gen-plugin-stub <dir> --ts\` for a TypeScript variant with \`tsc --watch\` build pipeline.
862
+
863
+ ## Live toolsmithing
864
+
865
+ Edit, save, hot-reload — Claude's next turn sees the new tool. See [documents/live-toolsmithing.md](https://github.com/Oolab-labs/patchwork-os/blob/main/documents/live-toolsmithing.md) for the full narrative.
866
+ `;
867
+ writeFileSync(path.join(outDir, "README.md"), readmeBody, "utf-8");
561
868
  // .gitignore
562
- writeFileSync(path.join(outDir, ".gitignore"), "node_modules\n", "utf-8");
563
- process.stderr.write(`✓ Plugin stub created at ${outDir}\n`);
869
+ const gitignore = useTypeScript
870
+ ? "node_modules\nindex.mjs\nindex.js\n*.tsbuildinfo\n"
871
+ : "node_modules\n";
872
+ writeFileSync(path.join(outDir, ".gitignore"), gitignore, "utf-8");
873
+ process.stderr.write(`✓ Plugin stub created at ${outDir} (${useTypeScript ? "TypeScript" : "JavaScript"})\n`);
564
874
  process.stderr.write("\nNext steps:\n");
565
- process.stderr.write(` 1. Edit ${path.join(outDir, "index.mjs")} to implement your tools\n`);
566
- process.stderr.write(` 2. Run the bridge with: claude-ide-bridge --plugin ${outDir}\n`);
567
- process.stderr.write(` 3. Or add to your config: { "plugins": ["${outDir}"] }\n`);
875
+ if (useTypeScript) {
876
+ process.stderr.write(` 1. cd ${outDir} && npm install\n`);
877
+ process.stderr.write(` 2. Edit ${path.join(outDir, "src", "index.ts")} to implement your tools\n`);
878
+ process.stderr.write(` 3. npm run dev (in one terminal)\n`);
879
+ process.stderr.write(` 4. claude-ide-bridge --plugin ${outDir} --plugin-watch (in another)\n`);
880
+ }
881
+ else {
882
+ process.stderr.write(` 1. Edit ${path.join(outDir, "index.mjs")} to implement your tools\n`);
883
+ process.stderr.write(` 2. Run the bridge with: claude-ide-bridge --plugin ${outDir} --plugin-watch\n`);
884
+ process.stderr.write(` 3. Or add to your config: { "plugins": ["${outDir}"] }\n`);
885
+ }
568
886
  process.exit(0);
569
887
  }
570
888
  // Patchwork: `patchwork recipe list` — enumerate installed recipes.
571
889
  if (process.argv[2] === "recipe" && process.argv[3] === "list") {
572
890
  (async () => {
573
- const { listYamlRecipes } = await import("./recipes/yamlRunner.js");
574
- const recipesDir = path.join(os.homedir(), ".patchwork", "recipes");
575
- const recipes = listYamlRecipes(recipesDir);
576
- if (recipes.length === 0) {
577
- process.stdout.write("No recipes installed. Run `patchwork-os patchwork-init` to install the starter set.\n");
891
+ const { listInstalledRecipes, printInstalledList } = await import("./commands/recipeInstall.js");
892
+ const entries = listInstalledRecipes();
893
+ printInstalledList(entries);
894
+ process.exit(0);
895
+ })();
896
+ }
897
+ // Patchwork: `patchwork recipe enable <name>` / `recipe disable <name>` —
898
+ // flip the disabled marker so scheduled triggers (cron/file-watch) take
899
+ // effect (or stop). Manual `recipe run` is unaffected.
900
+ if (process.argv[2] === "recipe" &&
901
+ (process.argv[3] === "enable" || process.argv[3] === "disable")) {
902
+ const subcommand = process.argv[3];
903
+ const name = process.argv[4];
904
+ if (!name) {
905
+ process.stderr.write(`Usage: patchwork recipe ${subcommand} <name>\n` +
906
+ ` See \`patchwork recipe list\` for installed recipe names.\n`);
907
+ process.exit(1);
908
+ }
909
+ (async () => {
910
+ try {
911
+ const { runRecipeEnable, runRecipeDisable } = await import("./commands/recipeInstall.js");
912
+ if (subcommand === "enable") {
913
+ const r = runRecipeEnable(name);
914
+ process.stdout.write(r.alreadyEnabled
915
+ ? ` ℹ ${r.name} is already enabled\n`
916
+ : ` ✓ enabled ${r.name}\n`);
917
+ }
918
+ else {
919
+ const r = runRecipeDisable(name);
920
+ process.stdout.write(r.alreadyDisabled
921
+ ? ` ℹ ${r.name} is already disabled\n`
922
+ : ` ✓ disabled ${r.name}\n`);
923
+ }
924
+ process.exit(0);
578
925
  }
579
- else {
580
- process.stdout.write(`Installed recipes (${recipes.length}):\n\n`);
581
- for (const r of recipes) {
582
- const desc = r.description ? ` ${r.description}` : "";
583
- process.stdout.write(` ${r.name.padEnd(28)} [${r.trigger}]${desc}\n`);
926
+ catch (err) {
927
+ process.stderr.write(`Error: ${err instanceof Error ? err.message : String(err)}\n`);
928
+ process.exit(1);
929
+ }
930
+ })();
931
+ }
932
+ // Patchwork: `patchwork recipe uninstall <name>` — remove an installed recipe
933
+ // directory and all its files. Sister to `recipe install`. Idempotent on
934
+ // success (subsequent uninstalls error with "no installed recipe").
935
+ if (process.argv[2] === "recipe" && process.argv[3] === "uninstall") {
936
+ const name = process.argv[4];
937
+ if (!name) {
938
+ process.stderr.write("Usage: patchwork recipe uninstall <name>\n" +
939
+ " See `patchwork recipe list` for installed recipe names.\n");
940
+ process.exit(1);
941
+ }
942
+ (async () => {
943
+ try {
944
+ const { runRecipeUninstall } = await import("./commands/recipeInstall.js");
945
+ const r = runRecipeUninstall(name);
946
+ if (!r.ok) {
947
+ process.stderr.write(`Error: ${r.error}\n`);
948
+ process.exit(1);
584
949
  }
585
- process.stdout.write(`\nRun a recipe: patchwork-os recipe run <name>\n`);
950
+ process.stdout.write(` ✓ Uninstalled ${name} (${r.installDir})\n`);
951
+ process.exit(0);
952
+ }
953
+ catch (err) {
954
+ process.stderr.write(`Error: ${err instanceof Error ? err.message : String(err)}\n`);
955
+ process.exit(1);
586
956
  }
587
- process.exit(0);
588
957
  })();
589
958
  }
590
959
  // Patchwork: `patchwork recipe run <name>` — runs a recipe locally or via
591
960
  // a running bridge's /recipes/run endpoint if one is available.
592
961
  if (process.argv[2] === "recipe" && process.argv[3] === "run") {
593
962
  const args = process.argv.slice(4);
594
- const localFlag = args.includes("--local");
595
- const name = args.find((a) => !a.startsWith("--"));
596
- if (!name) {
597
- process.stderr.write("Usage: patchwork recipe run <name> [--local]\n");
963
+ const usage = "Usage: patchwork recipe run <name-or-file> [--local] [--dry-run] [--step <id>] [--var KEY=VALUE]\n";
964
+ let localFlag = false;
965
+ let dryRun = false;
966
+ let recipeRef;
967
+ let step;
968
+ const vars = {};
969
+ for (let i = 0; i < args.length; i++) {
970
+ const arg = args[i];
971
+ if (arg === undefined) {
972
+ continue;
973
+ }
974
+ const currentArg = arg;
975
+ if (currentArg === "--local") {
976
+ localFlag = true;
977
+ continue;
978
+ }
979
+ if (currentArg === "--dry-run") {
980
+ dryRun = true;
981
+ continue;
982
+ }
983
+ if (currentArg === "--step" || currentArg.startsWith("--step=")) {
984
+ const value = currentArg === "--step"
985
+ ? args[++i]
986
+ : currentArg.slice("--step=".length);
987
+ if (!value) {
988
+ process.stderr.write(`Error: --step requires a value\n${usage}`);
989
+ process.exit(1);
990
+ }
991
+ step = value;
992
+ continue;
993
+ }
994
+ if (currentArg === "--var" || currentArg.startsWith("--var=")) {
995
+ const assignment = currentArg === "--var" ? args[++i] : currentArg.slice("--var=".length);
996
+ if (!assignment) {
997
+ process.stderr.write(`Error: --var requires KEY=VALUE\n${usage}`);
998
+ process.exit(1);
999
+ }
1000
+ const eqIndex = assignment.indexOf("=");
1001
+ if (eqIndex <= 0) {
1002
+ process.stderr.write(`Error: invalid --var assignment "${assignment}" (expected KEY=VALUE)\n${usage}`);
1003
+ process.exit(1);
1004
+ }
1005
+ const key = assignment.slice(0, eqIndex);
1006
+ const value = assignment.slice(eqIndex + 1);
1007
+ vars[key] = value;
1008
+ continue;
1009
+ }
1010
+ if (currentArg.startsWith("--")) {
1011
+ process.stderr.write(`Error: unknown option ${currentArg}\n${usage}`);
1012
+ process.exit(1);
1013
+ }
1014
+ if (!recipeRef) {
1015
+ recipeRef = currentArg;
1016
+ continue;
1017
+ }
1018
+ process.stderr.write(`Error: unexpected argument ${currentArg}\n${usage}`);
1019
+ process.exit(1);
1020
+ }
1021
+ if (!recipeRef) {
1022
+ process.stderr.write(usage);
598
1023
  process.exit(1);
599
1024
  }
1025
+ const recipeArg = recipeRef;
600
1026
  (async () => {
601
1027
  try {
602
- // Try bridge first (requires --claude-driver subprocess).
1028
+ const seedVars = Object.keys(vars).length > 0 ? vars : undefined;
1029
+ const explicitFile = (() => {
1030
+ try {
1031
+ const resolved = path.resolve(recipeArg);
1032
+ return existsSync(resolved) && statSync(resolved).isFile();
1033
+ }
1034
+ catch {
1035
+ return false;
1036
+ }
1037
+ })();
603
1038
  const { findBridgeLock } = await import("./bridgeLockDiscovery.js");
604
1039
  const lock = localFlag ? null : findBridgeLock();
605
- if (lock) {
1040
+ if (lock && !dryRun && !step && !explicitFile) {
606
1041
  const res = await fetch(`http://127.0.0.1:${lock.port}/recipes/run`, {
607
1042
  method: "POST",
608
1043
  headers: {
609
1044
  Authorization: `Bearer ${lock.authToken}`,
610
1045
  "Content-Type": "application/json",
611
1046
  },
612
- body: JSON.stringify({ name }),
1047
+ body: JSON.stringify({
1048
+ name: recipeArg,
1049
+ ...(seedVars ? { vars: seedVars } : {}),
1050
+ }),
613
1051
  });
614
1052
  const body = (await res.json());
615
1053
  if (!body.ok) {
@@ -622,43 +1060,64 @@ if (process.argv[2] === "recipe" && process.argv[3] === "run") {
622
1060
  // else: fall through to local runner below
623
1061
  }
624
1062
  else {
625
- process.stdout.write(` ✓ enqueued recipe "${name}" as task ${(body.taskId ?? "").slice(0, 8)}\n` +
1063
+ process.stdout.write(` ✓ enqueued recipe "${recipeArg}" as task ${(body.taskId ?? "").slice(0, 8)}\n` +
626
1064
  " Watch progress on the dashboard Tasks page or via listClaudeTasks.\n");
627
1065
  process.exit(0);
628
1066
  return;
629
1067
  }
630
1068
  }
631
- // No bridge run locally using the YAML runner.
632
- const { loadYamlRecipe, runYamlRecipe } = await import("./recipes/yamlRunner.js");
633
- const recipesDir = path.join(os.homedir(), ".patchwork", "recipes");
634
- const bundledDir = fileURLToPath(new URL("../templates/recipes", import.meta.url));
635
- const candidates = [
636
- path.join(recipesDir, `${name}.yaml`),
637
- path.join(recipesDir, `${name}.yml`),
638
- path.join(recipesDir, `${name}.json`),
639
- path.join(bundledDir, `${name}.yaml`),
640
- path.join(bundledDir, `${name}.yml`),
641
- ];
642
- let recipePath;
643
- for (const c of candidates) {
644
- if (existsSync(c)) {
645
- recipePath = c;
646
- break;
647
- }
648
- }
649
- if (!recipePath) {
650
- process.stderr.write(`Error: recipe "${name}" not found in ${recipesDir}\n` +
651
- " Run `patchwork-os recipe list` to see available recipes.\n");
652
- process.exit(1);
1069
+ const { runRecipe, runRecipeDryPlan, summarizeRecipeExecution, formatRunReport, extractRunLogStepResults, } = await import("./commands/recipe.js");
1070
+ if (dryRun) {
1071
+ const plan = await runRecipeDryPlan(recipeArg, {
1072
+ ...(step ? { step } : {}),
1073
+ ...(seedVars ? { vars: seedVars } : {}),
1074
+ });
1075
+ process.stdout.write(`${JSON.stringify(plan, null, 2)}\n`);
1076
+ process.exit(0);
653
1077
  return;
654
1078
  }
655
- process.stdout.write(` Running recipe "${name}" locally…\n`);
656
- const recipe = loadYamlRecipe(recipePath);
1079
+ process.stdout.write(step
1080
+ ? ` Running step "${step}" from recipe "${recipeArg}" locally…\n`
1081
+ : ` Running recipe "${recipeArg}" locally…\n`);
657
1082
  const workdir = lock?.workspace || process.cwd();
658
- const result = await runYamlRecipe(recipe, { workdir });
659
- process.stdout.write(` ✓ ${result.stepsRun} step(s) completed\n`);
660
- if (result.outputs.length > 0) {
661
- process.stdout.write(` Output written to:\n${result.outputs.map((o) => ` ${o}`).join("\n")}\n`);
1083
+ const run = await runRecipe(recipeArg, {
1084
+ ...(step ? { step } : {}),
1085
+ ...(seedVars ? { vars: seedVars } : {}),
1086
+ workdir,
1087
+ });
1088
+ if (run.stepSelection) {
1089
+ process.stdout.write(` Selected step via ${run.stepSelection.matchedBy}: ${run.stepSelection.matchedValue}\n`);
1090
+ }
1091
+ const summary = summarizeRecipeExecution(run.result);
1092
+ process.stdout.write(`${formatRunReport(run.result, run.recipe.name)}\n`);
1093
+ if (summary.errorMessage) {
1094
+ process.stderr.write(` Error: ${summary.errorMessage}\n`);
1095
+ }
1096
+ // Append to run log so CLI runs appear in ctxQueryTraces + dashboard /runs
1097
+ try {
1098
+ const { RecipeRunLog } = await import("./runLog.js");
1099
+ const runLog = new RecipeRunLog({
1100
+ dir: path.join(os.homedir(), ".patchwork"),
1101
+ });
1102
+ const startedAt = Date.now();
1103
+ const stepResultsForLog = extractRunLogStepResults(run.result);
1104
+ runLog.appendDirect({
1105
+ taskId: `cli-${Date.now()}`,
1106
+ recipeName: run.recipe.name,
1107
+ trigger: "recipe",
1108
+ status: summary.ok ? "done" : "error",
1109
+ createdAt: startedAt,
1110
+ startedAt,
1111
+ doneAt: Date.now(),
1112
+ durationMs: 0,
1113
+ ...(summary.errorMessage
1114
+ ? { errorMessage: summary.errorMessage }
1115
+ : {}),
1116
+ ...(stepResultsForLog ? { stepResults: stepResultsForLog } : {}),
1117
+ });
1118
+ }
1119
+ catch {
1120
+ // Non-fatal — run log write failure must not abort the CLI
662
1121
  }
663
1122
  process.exit(0);
664
1123
  }
@@ -669,24 +1128,596 @@ if (process.argv[2] === "recipe" && process.argv[3] === "run") {
669
1128
  })();
670
1129
  }
671
1130
  // Handle init subcommand — one-command setup: install extension + write CLAUDE.md + print next steps
672
- // Patchwork: `patchwork recipe install <file.json>` subcommand.
1131
+ // Patchwork: `patchwork recipe install <source>` subcommand.
1132
+ // Supports: github:owner/repo, github:owner/repo/subdir, https://github.com/owner/repo,
1133
+ // ./local/path, or legacy <file.json> (single-recipe install).
673
1134
  if (process.argv[2] === "recipe" && process.argv[3] === "install") {
1135
+ const source = process.argv[4];
1136
+ if (!source) {
1137
+ process.stderr.write("Usage: patchwork recipe install <source>\n" +
1138
+ " <source> can be:\n" +
1139
+ " github:owner/repo\n" +
1140
+ " github:owner/repo/subdir\n" +
1141
+ " https://github.com/owner/repo\n" +
1142
+ " ./local/path\n");
1143
+ process.exit(1);
1144
+ }
1145
+ (async () => {
1146
+ try {
1147
+ // Legacy path: bare .json file argument → single-file installer
1148
+ if (source.endsWith(".json") &&
1149
+ !source.startsWith("github:") &&
1150
+ !source.startsWith("http")) {
1151
+ const { installRecipeFromFile } = await import("./recipes/installer.js");
1152
+ const recipesDir = path.join(os.homedir(), ".patchwork", "recipes");
1153
+ const result = installRecipeFromFile(path.resolve(source), {
1154
+ recipesDir,
1155
+ });
1156
+ // alpha.36+ — sidecar `<name>.permissions.json` is no longer written
1157
+ // (was decorative, never read by toolRegistry). Print the suggested
1158
+ // permissions snippet inline so users can hand-merge into settings.
1159
+ process.stdout.write(` ✓ ${result.action} ${result.installedPath}\n` +
1160
+ ` ℹ Patchwork does not enforce per-recipe permissions; configure tool gating in ~/.claude/settings.json.\n` +
1161
+ ` Suggested permissions snippet:\n${result.permissionsJson
1162
+ .split("\n")
1163
+ .map((l) => ` ${l}`)
1164
+ .join("\n")}\n`);
1165
+ }
1166
+ else {
1167
+ // Marketplace install: github:, https://, ./local/
1168
+ const { runRecipeInstall, printInstallResult } = await import("./commands/recipeInstall.js");
1169
+ const result = await runRecipeInstall(source);
1170
+ printInstallResult(result);
1171
+ }
1172
+ process.exit(0);
1173
+ }
1174
+ catch (err) {
1175
+ process.stderr.write(`Error: ${err instanceof Error ? err.message : String(err)}\n`);
1176
+ process.exit(1);
1177
+ }
1178
+ })();
1179
+ }
1180
+ // Patchwork: `patchwork suggest [--since <date>]` — pattern-mine the
1181
+ // activity log + run history for "you've been doing X by hand; want to
1182
+ // make a recipe?" hints. See documents/strategic/2026-05-02/memory-
1183
+ // ecosystem-report.md §6 for the catalog this implements.
1184
+ //
1185
+ // Three suggestion kinds: co-occurring tool pairs (worth a recipe), tools
1186
+ // installed but unused (worth reviewing or pruning), and recipes that
1187
+ // always succeed (worth trust-graduating). Read-only — does not change
1188
+ // any policy or registry state.
1189
+ if (process.argv[2] === "suggest") {
1190
+ (async () => {
1191
+ try {
1192
+ const args = process.argv.slice(3);
1193
+ let sinceDays;
1194
+ for (let i = 0; i < args.length; i++) {
1195
+ const a = args[i];
1196
+ if (a === "--since-days") {
1197
+ const next = args[i + 1];
1198
+ if (next)
1199
+ sinceDays = Number.parseInt(next, 10);
1200
+ i++;
1201
+ }
1202
+ else if (a === "--help" || a === "-h") {
1203
+ process.stdout.write("patchwork suggest [--since-days <N>]\n\n" +
1204
+ "Pattern-mine the activity log + recipe runs for automation hints:\n" +
1205
+ " - Co-occurring tool pairs that don't yet appear in any recipe\n" +
1206
+ " - Installed tools that haven't been called recently\n" +
1207
+ " - Recipes that have succeeded ≥ 10 times in a row (trust-graduation candidates)\n\n" +
1208
+ "Default lookback window is 7 days. --since-days overrides.\n\n" +
1209
+ "Read-only — does not modify policy, registry, or run history.\n");
1210
+ process.exit(0);
1211
+ }
1212
+ }
1213
+ const { ActivityLog } = await import("./activityLog.js");
1214
+ const { RecipeRunLog } = await import("./runLog.js");
1215
+ const { computeAutomationSuggestions } = await import("./automationSuggestions.js");
1216
+ // Side-effect import — populates the tool registry that
1217
+ // computeAutomationSuggestions consults for installed-tool inventory.
1218
+ await import("./recipes/tools/index.js");
1219
+ // Wire up the bridge's standard log paths. The CLI reads from
1220
+ // disk; it doesn't need a running bridge.
1221
+ const patchworkDir = path.join(os.homedir(), ".patchwork");
1222
+ const activityLog = new ActivityLog();
1223
+ // Find the most recent activity log file (any port). For the
1224
+ // suggest CLI we union all of them.
1225
+ const claudeIdeDir = path.join(os.homedir(), ".claude", "ide");
1226
+ try {
1227
+ const entries = await import("node:fs").then((m) => m.readdirSync(claudeIdeDir));
1228
+ for (const name of entries) {
1229
+ if (/^activity(-\d+)?\.jsonl$/i.test(name)) {
1230
+ activityLog.setPersistPath(path.join(claudeIdeDir, name));
1231
+ break; // setPersistPath loads on call; first existing wins
1232
+ }
1233
+ }
1234
+ }
1235
+ catch {
1236
+ // No activity dir / files — proceed with an empty log; the
1237
+ // suggestions just return fewer / no items.
1238
+ }
1239
+ const recipeRunLog = new RecipeRunLog({ dir: patchworkDir });
1240
+ const opts = {
1241
+ activityLog,
1242
+ recipeRunLog,
1243
+ };
1244
+ if (sinceDays !== undefined && Number.isFinite(sinceDays)) {
1245
+ opts.activitySinceMs = sinceDays * 24 * 60 * 60 * 1000;
1246
+ }
1247
+ const suggestions = computeAutomationSuggestions(opts);
1248
+ if (suggestions.length === 0) {
1249
+ process.stdout.write("No automation suggestions yet. Patchwork mines patterns from the activity log\n" +
1250
+ "and recipe run history; come back after a few days of use.\n");
1251
+ process.exit(0);
1252
+ }
1253
+ process.stdout.write(`${suggestions.length} suggestion${suggestions.length === 1 ? "" : "s"}:\n\n`);
1254
+ for (const s of suggestions) {
1255
+ const icon = s.kind === "co_occurring_pair"
1256
+ ? "→"
1257
+ : s.kind === "installed_but_unused"
1258
+ ? "·"
1259
+ : "★";
1260
+ process.stdout.write(` ${icon} ${s.label}\n`);
1261
+ }
1262
+ process.stdout.write("\nRead-only output. Nothing changed.\n");
1263
+ process.exit(0);
1264
+ }
1265
+ catch (err) {
1266
+ process.stderr.write(`Error: ${err instanceof Error ? err.message : String(err)}\n`);
1267
+ process.exit(1);
1268
+ }
1269
+ })();
1270
+ }
1271
+ // Patchwork: `patchwork recipe schema [outputDir]` — write generated recipe schemas to disk.
1272
+ // Patchwork: `patchwork traces export [--output <path>]` — bundle the four
1273
+ // local trace logs into a single .jsonl.gz so a user can move machines,
1274
+ // take a compliance snapshot, or share traces with another tool. See
1275
+ // docs/strategic/2026-05-02/memory-ecosystem-report.md items 1, 3, 12 for
1276
+ // the durability rationale this PR addresses.
1277
+ if (process.argv[2] === "traces" && process.argv[3] === "export") {
1278
+ (async () => {
1279
+ try {
1280
+ const args = process.argv.slice(4);
1281
+ let output;
1282
+ let patchworkDir;
1283
+ let activityDir;
1284
+ for (let i = 0; i < args.length; i++) {
1285
+ const a = args[i];
1286
+ if (a === "--output" || a === "-o") {
1287
+ output = args[i + 1];
1288
+ i++;
1289
+ }
1290
+ else if (a === "--patchwork-dir") {
1291
+ patchworkDir = args[i + 1];
1292
+ i++;
1293
+ }
1294
+ else if (a === "--activity-dir") {
1295
+ activityDir = args[i + 1];
1296
+ i++;
1297
+ }
1298
+ else if (a === "--help" || a === "-h") {
1299
+ process.stdout.write("patchwork traces export [--output <path>] [--patchwork-dir <dir>] [--activity-dir <dir>]\n\n" +
1300
+ "Bundles ~/.patchwork/{runs,decision_traces,commit_issue_links}.jsonl\n" +
1301
+ "and ~/.claude/ide/activity-*.jsonl into a single gzipped JSONL file.\n\n" +
1302
+ "Output is a manifest line followed by one envelope per row:\n" +
1303
+ ' {"type":"manifest", ...}\n' +
1304
+ ' {"source":"runs", "entry":{...}}\n' +
1305
+ " ...\n\n" +
1306
+ "Filter one source with:\n" +
1307
+ " gunzip -c traces-export-*.jsonl.gz | jq 'select(.source==\"decision_traces\") | .entry'\n");
1308
+ process.exit(0);
1309
+ }
1310
+ }
1311
+ const { runTracesExport } = await import("./commands/tracesExport.js");
1312
+ const result = await runTracesExport({
1313
+ ...(output !== undefined && { output }),
1314
+ ...(patchworkDir !== undefined && { patchworkDir }),
1315
+ ...(activityDir !== undefined && { activityDir }),
1316
+ });
1317
+ process.stdout.write(` ✓ Wrote ${result.outputPath}\n`);
1318
+ process.stdout.write(` ${result.totalCount} rows from ${result.files.length} file${result.files.length === 1 ? "" : "s"} (${result.totalBytes} bytes read)\n`);
1319
+ for (const f of result.files) {
1320
+ process.stdout.write(` - ${f.source}: ${f.count} rows (${f.path})\n`);
1321
+ }
1322
+ process.exit(0);
1323
+ }
1324
+ catch (err) {
1325
+ process.stderr.write(`Error: ${err instanceof Error ? err.message : String(err)}\n`);
1326
+ process.exit(1);
1327
+ }
1328
+ })();
1329
+ }
1330
+ // Patchwork: `patchwork traces import <bundle>` — restore an export bundle
1331
+ // into the local patchwork dirs. Closes the half-shipped backup loop.
1332
+ if (process.argv[2] === "traces" && process.argv[3] === "import") {
1333
+ (async () => {
1334
+ try {
1335
+ const args = process.argv.slice(4);
1336
+ let input;
1337
+ let patchworkDir;
1338
+ let activityDir;
1339
+ let mode = "append";
1340
+ let dryRun = false;
1341
+ let passphrase;
1342
+ for (let i = 0; i < args.length; i++) {
1343
+ const a = args[i];
1344
+ if (a === "--patchwork-dir") {
1345
+ patchworkDir = args[i + 1];
1346
+ i++;
1347
+ }
1348
+ else if (a === "--activity-dir") {
1349
+ activityDir = args[i + 1];
1350
+ i++;
1351
+ }
1352
+ else if (a === "--mode") {
1353
+ const m = args[i + 1];
1354
+ if (m !== "append" && m !== "overwrite") {
1355
+ process.stderr.write(`Error: --mode must be "append" or "overwrite" (got: ${m})\n`);
1356
+ process.exit(1);
1357
+ }
1358
+ mode = m;
1359
+ i++;
1360
+ }
1361
+ else if (a === "--passphrase") {
1362
+ passphrase = args[i + 1];
1363
+ i++;
1364
+ }
1365
+ else if (a === "--dry-run") {
1366
+ dryRun = true;
1367
+ }
1368
+ else if (a === "--help" || a === "-h") {
1369
+ process.stdout.write("patchwork traces import <bundle> [--mode append|overwrite] [--dry-run]\n" +
1370
+ " [--passphrase <phrase>]\n" +
1371
+ " [--patchwork-dir <dir>] [--activity-dir <dir>]\n\n" +
1372
+ "Restore a bundle written by `patchwork traces export` into the local\n" +
1373
+ "patchwork dirs (~/.patchwork/ and ~/.claude/ide/ by default).\n\n" +
1374
+ "Formats:\n" +
1375
+ " .jsonl.gz Plain gzip bundle — no passphrase required.\n" +
1376
+ " .enc AES-256-GCM encrypted bundle — pass --passphrase.\n\n" +
1377
+ "Modes:\n" +
1378
+ " append (default) Append rows to existing files.\n" +
1379
+ " overwrite Truncate target files before writing. Use for fresh-machine\n" +
1380
+ " restore; never use when there's local data you want to keep.\n");
1381
+ process.exit(0);
1382
+ }
1383
+ else if (a !== undefined &&
1384
+ !a.startsWith("--") &&
1385
+ input === undefined) {
1386
+ input = a;
1387
+ }
1388
+ }
1389
+ if (!input) {
1390
+ process.stderr.write("Usage: patchwork traces import <bundle> [--passphrase <phrase>] [--mode append|overwrite] [--dry-run]\n");
1391
+ process.exit(1);
1392
+ }
1393
+ // Auto-detect encrypted bundle and decrypt before import.
1394
+ if (passphrase !== undefined || input.endsWith(".enc")) {
1395
+ const { readFileSync } = await import("node:fs");
1396
+ const { isEncryptedTraceBundle, decryptTraceBundle } = await import("./traceEncryption.js");
1397
+ const raw = readFileSync(input);
1398
+ if (isEncryptedTraceBundle(raw)) {
1399
+ if (!passphrase) {
1400
+ process.stderr.write("Error: bundle is encrypted — provide --passphrase <phrase>\n");
1401
+ process.exit(1);
1402
+ }
1403
+ const plain = decryptTraceBundle(raw, passphrase);
1404
+ const { tmpdir } = await import("node:os");
1405
+ const { writeFileSync } = await import("node:fs");
1406
+ const tmp = `${tmpdir()}/patchwork-import-${Date.now()}.jsonl.gz`;
1407
+ writeFileSync(tmp, plain, { mode: 0o600 });
1408
+ input = tmp;
1409
+ process.stderr.write("Decryption succeeded.\n");
1410
+ }
1411
+ }
1412
+ const { runTracesImport } = await import("./commands/tracesImport.js");
1413
+ const result = await runTracesImport({
1414
+ input,
1415
+ ...(patchworkDir !== undefined && { patchworkDir }),
1416
+ ...(activityDir !== undefined && { activityDir }),
1417
+ mode,
1418
+ dryRun,
1419
+ });
1420
+ const verb = result.dryRun
1421
+ ? "Would restore"
1422
+ : result.mode === "overwrite"
1423
+ ? "Restored (overwrite)"
1424
+ : "Restored (append)";
1425
+ process.stdout.write(` ${result.dryRun ? "•" : "✓"} ${verb} ${result.totalCount} rows from ${result.inputPath}\n`);
1426
+ process.stdout.write(` Bundle exportedAt: ${result.exportedAt}\n`);
1427
+ for (const f of result.files) {
1428
+ process.stdout.write(` - ${f.source}: ${f.count} rows → ${f.targetPath}\n`);
1429
+ }
1430
+ process.exit(0);
1431
+ }
1432
+ catch (err) {
1433
+ process.stderr.write(`Error: ${err instanceof Error ? err.message : String(err)}\n`);
1434
+ process.exit(1);
1435
+ }
1436
+ })();
1437
+ }
1438
+ if (process.argv[2] === "recipe" && process.argv[3] === "schema") {
1439
+ const outputDir = process.argv[4] ?? path.join(process.cwd(), "schemas");
1440
+ (async () => {
1441
+ try {
1442
+ const { runSchema } = await import("./commands/recipe.js");
1443
+ const result = await runSchema(path.resolve(outputDir));
1444
+ process.stdout.write(` ✓ Wrote schemas to ${result.outputDir}\n`);
1445
+ for (const file of result.filesWritten) {
1446
+ process.stdout.write(` ${file}\n`);
1447
+ }
1448
+ process.exit(0);
1449
+ }
1450
+ catch (err) {
1451
+ process.stderr.write(`Error: ${err instanceof Error ? err.message : String(err)}\n`);
1452
+ process.exit(1);
1453
+ }
1454
+ })();
1455
+ }
1456
+ // Patchwork: `patchwork recipe new <name>` — scaffold a new recipe from template.
1457
+ if (process.argv[2] === "recipe" && process.argv[3] === "new") {
1458
+ const args = process.argv.slice(4);
1459
+ const recipeName = args[0];
1460
+ if (!recipeName) {
1461
+ process.stderr.write("Usage: patchwork recipe new <name> [--template <name>] [--desc <description>] [--out <dir>]\n" +
1462
+ " --out <dir> Write the recipe to <dir>/<name>.yaml.\n" +
1463
+ " Defaults to ~/.patchwork/recipes/ — pass `--out .` to\n" +
1464
+ " write into the current directory instead.\n");
1465
+ process.stderr.write("\nTemplates:\n");
1466
+ (async () => {
1467
+ const { listTemplates } = await import("./commands/recipe.js");
1468
+ for (const t of listTemplates()) {
1469
+ process.stderr.write(` ${t}\n`);
1470
+ }
1471
+ process.exit(1);
1472
+ })();
1473
+ }
1474
+ else {
1475
+ (async () => {
1476
+ try {
1477
+ const { runNew } = await import("./commands/recipe.js");
1478
+ const templateIdx = args.indexOf("--template");
1479
+ const template = templateIdx >= 0 ? args[templateIdx + 1] : undefined;
1480
+ const descIdx = args.indexOf("--desc");
1481
+ const description = (descIdx >= 0 ? args[descIdx + 1] : undefined) ??
1482
+ `Recipe: ${recipeName}`;
1483
+ const outIdx = args.indexOf("--out");
1484
+ const outRaw = outIdx >= 0 ? args[outIdx + 1] : undefined;
1485
+ // `--out .` is the common case for "scaffold in cwd" — resolve so
1486
+ // the success message shows the absolute path the user can open.
1487
+ const outputDir = outRaw ? path.resolve(outRaw) : undefined;
1488
+ const result = runNew({
1489
+ name: recipeName,
1490
+ description,
1491
+ ...(template ? { template } : {}),
1492
+ ...(outputDir ? { outputDir } : {}),
1493
+ });
1494
+ process.stdout.write(` ✓ Created ${result.path}\n`);
1495
+ process.exit(0);
1496
+ }
1497
+ catch (err) {
1498
+ process.stderr.write(`Error: ${err instanceof Error ? err.message : String(err)}\n`);
1499
+ process.exit(1);
1500
+ }
1501
+ })();
1502
+ }
1503
+ }
1504
+ // Patchwork: `patchwork recipe lint <file.yaml>` — validate recipe against schema.
1505
+ if (process.argv[2] === "recipe" && process.argv[3] === "lint") {
674
1506
  const file = process.argv[4];
675
1507
  if (!file) {
676
- process.stderr.write("Usage: patchwork recipe install <file.json>\n");
1508
+ process.stderr.write("Usage: patchwork recipe lint <file.yaml>\n");
677
1509
  process.exit(1);
678
1510
  }
679
1511
  (async () => {
680
1512
  try {
681
- const { installRecipeFromFile } = await import("./recipes/installer.js");
682
- const recipesDir = path.join(os.homedir(), ".patchwork", "recipes");
683
- const result = installRecipeFromFile(path.resolve(file), {
684
- recipesDir,
1513
+ const { runLint } = await import("./commands/recipe.js");
1514
+ const result = runLint(path.resolve(file));
1515
+ for (const issue of result.issues) {
1516
+ const prefix = issue.level === "error" ? "✗" : "⚠";
1517
+ process.stderr.write(` ${prefix} ${issue.message}\n`);
1518
+ }
1519
+ if (result.valid) {
1520
+ process.stdout.write(` ✓ Valid recipe (${result.warnings} warnings)\n`);
1521
+ process.exit(0);
1522
+ }
1523
+ else {
1524
+ process.stdout.write(`\n ${result.errors} error(s), ${result.warnings} warning(s)\n`);
1525
+ process.exit(1);
1526
+ }
1527
+ }
1528
+ catch (err) {
1529
+ process.stderr.write(`Error: ${err instanceof Error ? err.message : String(err)}\n`);
1530
+ process.exit(1);
1531
+ }
1532
+ })();
1533
+ }
1534
+ // Patchwork: `patchwork recipe preflight <file.yaml>` — static policy check (lint + plan + writes + fixtures).
1535
+ if (process.argv[2] === "recipe" && process.argv[3] === "preflight") {
1536
+ const args = process.argv.slice(4);
1537
+ const usage = "Usage: patchwork recipe preflight <file.yaml> [--json] [--watch] [--require-fixtures] [--no-require-write-ack] [--allow-write <tool-or-ns>]\n";
1538
+ let json = false;
1539
+ let watchMode = false;
1540
+ let requireFixtures = false;
1541
+ let requireWriteAck = true;
1542
+ const allowWrites = [];
1543
+ let file;
1544
+ for (let i = 0; i < args.length; i++) {
1545
+ const arg = args[i];
1546
+ if (arg === undefined)
1547
+ continue;
1548
+ if (arg === "--json") {
1549
+ json = true;
1550
+ continue;
1551
+ }
1552
+ if (arg === "--watch") {
1553
+ watchMode = true;
1554
+ continue;
1555
+ }
1556
+ if (arg === "--require-fixtures") {
1557
+ requireFixtures = true;
1558
+ continue;
1559
+ }
1560
+ if (arg === "--no-require-write-ack") {
1561
+ requireWriteAck = false;
1562
+ continue;
1563
+ }
1564
+ if (arg === "--allow-write" || arg.startsWith("--allow-write=")) {
1565
+ const value = arg === "--allow-write"
1566
+ ? args[++i]
1567
+ : arg.slice("--allow-write=".length);
1568
+ if (!value) {
1569
+ process.stderr.write(`Error: --allow-write requires a value\n${usage}`);
1570
+ process.exit(1);
1571
+ }
1572
+ allowWrites.push(value);
1573
+ continue;
1574
+ }
1575
+ if (!arg.startsWith("--")) {
1576
+ file = arg;
1577
+ }
1578
+ }
1579
+ if (!file) {
1580
+ process.stderr.write(usage);
1581
+ process.exit(1);
1582
+ }
1583
+ const renderResult = (result) => {
1584
+ if (json) {
1585
+ process.stdout.write(`${JSON.stringify(result, null, 2)}\n`);
1586
+ return;
1587
+ }
1588
+ for (const issue of result.issues) {
1589
+ const prefix = issue.level === "error" ? "✗" : "⚠";
1590
+ const where = issue.stepId ? ` [${issue.stepId}]` : "";
1591
+ process.stderr.write(` ${prefix} ${issue.code}${where}: ${issue.message}\n`);
1592
+ }
1593
+ if (result.ok) {
1594
+ process.stdout.write(` ✓ Preflight passed for ${result.recipe} (${result.plan.steps.length} steps)\n`);
1595
+ }
1596
+ else {
1597
+ const errorCount = result.issues.filter((i) => i.level === "error").length;
1598
+ process.stdout.write(`\n ${errorCount} error(s) — preflight failed\n`);
1599
+ }
1600
+ };
1601
+ (async () => {
1602
+ try {
1603
+ const { runPreflight, runPreflightWatch } = await import("./commands/recipe.js");
1604
+ const resolvedPath = path.resolve(file);
1605
+ if (watchMode) {
1606
+ process.stdout.write(` Watching ${resolvedPath} — preflight on save…\n`);
1607
+ const stop = runPreflightWatch({
1608
+ recipePath: resolvedPath,
1609
+ requireWriteAck,
1610
+ requireFixtures,
1611
+ allowWrites,
1612
+ onResult: (result) => renderResult(result),
1613
+ onError: (err) => {
1614
+ process.stderr.write(`Error: ${err.message}\n`);
1615
+ },
1616
+ });
1617
+ process.on("SIGINT", () => {
1618
+ stop();
1619
+ process.exit(0);
1620
+ });
1621
+ return;
1622
+ }
1623
+ const result = await runPreflight(resolvedPath, {
1624
+ requireWriteAck,
1625
+ requireFixtures,
1626
+ allowWrites,
685
1627
  });
686
- process.stdout.write(` ✓ ${result.action} ${result.installedPath}\n` +
687
- ` ℹ permissions snippet written to ${result.installedPath}.permissions.json\n` +
688
- ` Review + merge into ~/.claude/settings.json to pre-approve recipe steps.\n`);
689
- process.exit(0);
1628
+ renderResult(result);
1629
+ process.exit(result.ok ? 0 : 1);
1630
+ }
1631
+ catch (err) {
1632
+ process.stderr.write(`Error: ${err instanceof Error ? err.message : String(err)}\n`);
1633
+ process.exit(1);
1634
+ }
1635
+ })();
1636
+ }
1637
+ // Patchwork: `patchwork recipe fmt <file.yaml>` — format/normalize recipe.
1638
+ if (process.argv[2] === "recipe" && process.argv[3] === "fmt") {
1639
+ const args = process.argv.slice(4);
1640
+ const check = args.includes("--check");
1641
+ const watchMode = args.includes("--watch");
1642
+ const file = args.find((arg) => !arg.startsWith("--"));
1643
+ if (!file) {
1644
+ process.stderr.write("Usage: patchwork recipe fmt <file.yaml> [--check] [--watch]\n");
1645
+ process.exit(1);
1646
+ }
1647
+ const renderResult = (result, filePath) => {
1648
+ if (check) {
1649
+ process.stdout.write(result.changed
1650
+ ? " ✗ File would be reformatted\n"
1651
+ : " ✓ File is already formatted\n");
1652
+ }
1653
+ else {
1654
+ process.stdout.write(result.changed
1655
+ ? ` ✓ Formatted ${filePath}\n`
1656
+ : ` ✓ Already formatted ${filePath}\n`);
1657
+ }
1658
+ };
1659
+ (async () => {
1660
+ try {
1661
+ const { runFmt, runFmtWatch } = await import("./commands/recipe.js");
1662
+ const resolvedPath = path.resolve(file);
1663
+ if (watchMode) {
1664
+ process.stdout.write(` Watching ${resolvedPath} — fmt on save…\n`);
1665
+ const stop = runFmtWatch({
1666
+ recipePath: resolvedPath,
1667
+ check,
1668
+ onResult: (result) => {
1669
+ process.stdout.write(`\n[${new Date().toLocaleTimeString()}] ${resolvedPath}\n`);
1670
+ renderResult(result, resolvedPath);
1671
+ },
1672
+ onError: (err) => {
1673
+ process.stderr.write(`Error: ${err.message}\n`);
1674
+ },
1675
+ });
1676
+ process.on("SIGINT", () => {
1677
+ stop();
1678
+ process.exit(0);
1679
+ });
1680
+ return;
1681
+ }
1682
+ const result = runFmt(resolvedPath, { check });
1683
+ renderResult(result, file);
1684
+ process.exit(check && result.changed ? 1 : 0);
1685
+ }
1686
+ catch (err) {
1687
+ process.stderr.write(`Error: ${err instanceof Error ? err.message : String(err)}\n`);
1688
+ process.exit(1);
1689
+ }
1690
+ })();
1691
+ }
1692
+ // Patchwork: `patchwork recipe record <file.yaml>` — execute live and record connector fixtures.
1693
+ if (process.argv[2] === "recipe" && process.argv[3] === "record") {
1694
+ const args = process.argv.slice(4);
1695
+ const file = args.find((arg) => !arg.startsWith("--"));
1696
+ const fixturesIdx = args.indexOf("--fixtures");
1697
+ const fixturesDir = fixturesIdx >= 0 ? args[fixturesIdx + 1] : undefined;
1698
+ if (!file) {
1699
+ process.stderr.write("Usage: patchwork recipe record <file.yaml> [--fixtures <dir>]\n");
1700
+ process.exit(1);
1701
+ }
1702
+ (async () => {
1703
+ try {
1704
+ const { runRecord } = await import("./commands/recipe.js");
1705
+ const result = await runRecord(path.resolve(file), {
1706
+ ...(fixturesDir ? { fixturesDir: path.resolve(fixturesDir) } : {}),
1707
+ });
1708
+ for (const issue of result.issues) {
1709
+ const prefix = issue.level === "error" ? "✗" : "⚠";
1710
+ process.stderr.write(` ${prefix} ${issue.message}\n`);
1711
+ }
1712
+ if (result.recordedFixtures.length > 0) {
1713
+ process.stdout.write(` ℹ Recorded fixture libraries: ${result.recordedFixtures.join(", ")}\n`);
1714
+ }
1715
+ if (result.valid) {
1716
+ process.stdout.write(" ✓ Recipe fixtures recorded\n");
1717
+ process.exit(0);
1718
+ }
1719
+ process.stdout.write(`\n ${result.errors} error(s), ${result.warnings} warning(s)\n`);
1720
+ process.exit(1);
690
1721
  }
691
1722
  catch (err) {
692
1723
  process.stderr.write(`Error: ${err instanceof Error ? err.message : String(err)}\n`);
@@ -694,6 +1725,153 @@ if (process.argv[2] === "recipe" && process.argv[3] === "install") {
694
1725
  }
695
1726
  })();
696
1727
  }
1728
+ // Patchwork: `patchwork recipe test <file.yaml>` — validate fixture coverage for mocked execution.
1729
+ if (process.argv[2] === "recipe" && process.argv[3] === "test") {
1730
+ const args = process.argv.slice(4);
1731
+ const file = args.find((arg) => !arg.startsWith("--"));
1732
+ const fixturesIdx = args.indexOf("--fixtures");
1733
+ const fixturesDir = fixturesIdx >= 0 ? args[fixturesIdx + 1] : undefined;
1734
+ const watchMode = args.includes("--watch");
1735
+ if (!file) {
1736
+ process.stderr.write("Usage: patchwork recipe test <file.yaml> [--fixtures <dir>] [--watch]\n");
1737
+ process.exit(1);
1738
+ }
1739
+ const renderResult = (result) => {
1740
+ for (const issue of result.issues) {
1741
+ const prefix = issue.level === "error" ? "✗" : "⚠";
1742
+ process.stderr.write(` ${prefix} ${issue.message}\n`);
1743
+ }
1744
+ if (result.requiredFixtures.length > 0) {
1745
+ process.stdout.write(` ℹ Required fixtures: ${result.requiredFixtures.join(", ")}\n`);
1746
+ }
1747
+ if (result.valid) {
1748
+ process.stdout.write(" ✓ Test passed\n");
1749
+ }
1750
+ else {
1751
+ process.stdout.write(`\n ${result.errors} error(s), ${result.warnings} warning(s)\n`);
1752
+ }
1753
+ };
1754
+ (async () => {
1755
+ try {
1756
+ const { runTest, runTestWatch } = await import("./commands/recipe.js");
1757
+ const resolvedPath = path.resolve(file);
1758
+ const resolvedFixtures = fixturesDir
1759
+ ? path.resolve(fixturesDir)
1760
+ : undefined;
1761
+ if (watchMode) {
1762
+ process.stdout.write(` Watching ${resolvedPath} — test on save…\n`);
1763
+ const stop = runTestWatch({
1764
+ recipePath: resolvedPath,
1765
+ ...(resolvedFixtures ? { fixturesDir: resolvedFixtures } : {}),
1766
+ onResult: (result) => {
1767
+ process.stdout.write(`\n[${new Date().toLocaleTimeString()}] ${resolvedPath}\n`);
1768
+ renderResult(result);
1769
+ },
1770
+ onError: (err) => {
1771
+ process.stderr.write(`Error: ${err.message}\n`);
1772
+ },
1773
+ });
1774
+ process.on("SIGINT", () => {
1775
+ stop();
1776
+ process.exit(0);
1777
+ });
1778
+ return;
1779
+ }
1780
+ const result = await runTest(resolvedPath, {
1781
+ ...(resolvedFixtures ? { fixturesDir: resolvedFixtures } : {}),
1782
+ });
1783
+ renderResult(result);
1784
+ process.exit(result.valid ? 0 : 1);
1785
+ }
1786
+ catch (err) {
1787
+ process.stderr.write(`Error: ${err instanceof Error ? err.message : String(err)}\n`);
1788
+ process.exit(1);
1789
+ }
1790
+ })();
1791
+ }
1792
+ // Patchwork: `patchwork recipe watch <file.yaml>` — watch for changes and validate.
1793
+ if (process.argv[2] === "recipe" && process.argv[3] === "watch") {
1794
+ const file = process.argv[4];
1795
+ if (!file) {
1796
+ process.stderr.write("Usage: patchwork recipe watch <file.yaml>\n");
1797
+ process.exit(1);
1798
+ }
1799
+ (async () => {
1800
+ const { findBridgeLock } = await import("./bridgeLockDiscovery.js");
1801
+ const { runWatch, runLint, runWatchedRecipe, formatRunReport, summarizeRecipeExecution, extractRunLogStepResults, } = await import("./commands/recipe.js");
1802
+ const filePath = path.resolve(file);
1803
+ const lock = findBridgeLock();
1804
+ const workdir = lock?.workspace || process.cwd();
1805
+ const initial = runLint(filePath);
1806
+ if (!initial.valid) {
1807
+ process.stderr.write(" ✗ Recipe has errors - fix before watching\n");
1808
+ for (const issue of initial.issues) {
1809
+ process.stderr.write(` ${issue.level}: ${issue.message}\n`);
1810
+ }
1811
+ }
1812
+ else {
1813
+ process.stdout.write(` ✓ Watching ${file} for changes...\n`);
1814
+ }
1815
+ const stop = runWatch({
1816
+ recipePath: filePath,
1817
+ onChange: async () => {
1818
+ process.stdout.write(`\n Change detected, running...\n`);
1819
+ const watched = await runWatchedRecipe(filePath, { workdir });
1820
+ if (!watched.lint.valid) {
1821
+ process.stderr.write(` ✗ Invalid (${watched.lint.errors} errors)\n`);
1822
+ for (const issue of watched.lint.issues) {
1823
+ process.stderr.write(` ${issue.level}: ${issue.message}\n`);
1824
+ }
1825
+ return;
1826
+ }
1827
+ if (watched.run?.stepSelection) {
1828
+ process.stdout.write(` Selected step via ${watched.run.stepSelection.matchedBy}: ${watched.run.stepSelection.matchedValue}\n`);
1829
+ }
1830
+ if (watched.run) {
1831
+ process.stdout.write(`${formatRunReport(watched.run.result, watched.run.recipe.name)}\n`);
1832
+ const summary = summarizeRecipeExecution(watched.run.result);
1833
+ if (summary.errorMessage) {
1834
+ process.stderr.write(` Error: ${summary.errorMessage}\n`);
1835
+ }
1836
+ // Append to run log
1837
+ try {
1838
+ const { RecipeRunLog } = await import("./runLog.js");
1839
+ const runLog = new RecipeRunLog({
1840
+ dir: path.join(os.homedir(), ".patchwork"),
1841
+ });
1842
+ const now = Date.now();
1843
+ const stepResultsForLog = extractRunLogStepResults(watched.run.result);
1844
+ runLog.appendDirect({
1845
+ taskId: `watch-${now}`,
1846
+ recipeName: watched.run.recipe.name,
1847
+ trigger: "recipe",
1848
+ status: summary.ok ? "done" : "error",
1849
+ createdAt: now,
1850
+ startedAt: now,
1851
+ doneAt: now,
1852
+ durationMs: 0,
1853
+ ...(summary.errorMessage
1854
+ ? { errorMessage: summary.errorMessage }
1855
+ : {}),
1856
+ ...(stepResultsForLog ? { stepResults: stepResultsForLog } : {}),
1857
+ });
1858
+ }
1859
+ catch {
1860
+ // non-fatal
1861
+ }
1862
+ }
1863
+ },
1864
+ onError: (err) => {
1865
+ process.stderr.write(` Error: ${err.message}\n`);
1866
+ },
1867
+ });
1868
+ process.on("SIGINT", () => {
1869
+ process.stdout.write("\n Stopping watch...\n");
1870
+ stop();
1871
+ process.exit(0);
1872
+ });
1873
+ })();
1874
+ }
697
1875
  if (process.argv[2] === "init") {
698
1876
  const argv = process.argv.slice(3);
699
1877
  // Handle init --help
@@ -1383,6 +2561,23 @@ Options:
1383
2561
  }
1384
2562
  process.exit(0);
1385
2563
  }
2564
+ // Handle launchd subcommand — install/uninstall macOS LaunchAgent for auto-start
2565
+ if (process.argv[2] === "launchd") {
2566
+ const sub = process.argv[3];
2567
+ if (sub === "install") {
2568
+ const { runLaunchdInstall } = await import("./commands/launchd.js");
2569
+ await runLaunchdInstall(process.argv.slice(4));
2570
+ }
2571
+ else if (sub === "uninstall") {
2572
+ const { runLaunchdUninstall } = await import("./commands/launchd.js");
2573
+ await runLaunchdUninstall(process.argv.slice(4));
2574
+ }
2575
+ else {
2576
+ process.stderr.write("Usage: patchwork-os launchd install|uninstall\n");
2577
+ process.exit(1);
2578
+ }
2579
+ process.exit(0);
2580
+ }
1386
2581
  // F6: "Did you mean?" for unknown CLI subcommands
1387
2582
  // Patchwork: no-args → terminal dashboard (when invoked as patchwork-os or patchwork).
1388
2583
  {
@@ -1390,7 +2585,7 @@ Options:
1390
2585
  const isPatchworkBin = binName === "patchwork-os" ||
1391
2586
  binName === "patchwork" ||
1392
2587
  binName === "patchwork.js";
1393
- if (isPatchworkBin && !process.argv[2]) {
2588
+ if (isPatchworkBin && (!process.argv[2] || process.argv[2] === "dashboard")) {
1394
2589
  (async () => {
1395
2590
  const { runDashboard } = await import("./commands/dashboard.js");
1396
2591
  await runDashboard();
@@ -1398,26 +2593,12 @@ Options:
1398
2593
  }
1399
2594
  }
1400
2595
  {
1401
- const KNOWN_COMMANDS = [
1402
- "init",
1403
- "patchwork-init",
1404
- "start-all",
1405
- "install-extension",
1406
- "gen-claude-md",
1407
- "print-token",
1408
- "gen-plugin-stub",
1409
- "notify",
1410
- "install",
1411
- "marketplace",
1412
- "status",
1413
- "shim",
1414
- "recipe",
1415
- "dashboard",
1416
- ];
2596
+ // Reuses the KNOWN_SUBCOMMANDS list from the top of this file as a single
2597
+ // source of truth for "what subcommand argv tokens are recognized".
1417
2598
  const unknownSub = process.argv[2];
1418
2599
  if (unknownSub &&
1419
2600
  !unknownSub.startsWith("-") &&
1420
- !KNOWN_COMMANDS.includes(unknownSub)) {
2601
+ !KNOWN_SUBCOMMANDS.includes(unknownSub)) {
1421
2602
  const lev = (a, b) => {
1422
2603
  const dp = Array.from({ length: a.length + 1 }, (_, i) => Array.from({ length: b.length + 1 }, (_, j) => i === 0 ? j : j === 0 ? i : 0));
1423
2604
  for (let i = 1; i <= a.length; i++)
@@ -1433,127 +2614,146 @@ Options:
1433
2614
  // biome-ignore lint/style/noNonNullAssertion: dp is fully pre-allocated
1434
2615
  return dp[a.length][b.length];
1435
2616
  };
1436
- const closest = [...KNOWN_COMMANDS].sort((a, b) => lev(unknownSub, a) - lev(unknownSub, b))[0];
2617
+ const closest = [...KNOWN_SUBCOMMANDS].sort((a, b) => lev(unknownSub, a) - lev(unknownSub, b))[0];
1437
2618
  console.error(`Unknown command: '${unknownSub}'. Did you mean: ${closest}?`);
1438
2619
  process.exit(1);
1439
2620
  }
1440
2621
  }
1441
- const config = parseConfig(process.argv);
1442
- // Patchwork: resolve --model flag (optional, non-invasive) stashes the
1443
- // configured adapter on globalThis for consumers that opt into the adapter
1444
- // layer. Bridge subprocess driver still works when --model is absent.
1445
- try {
1446
- const { resolveModel } = await import("./patchworkCli.js");
1447
- const resolved = resolveModel(process.argv);
1448
- if (resolved) {
1449
- globalThis.__patchworkAdapter =
1450
- resolved.adapter;
1451
- process.stderr.write(`[patchwork] model adapter initialized: ${resolved.adapter.name}\n`);
1452
- }
1453
- }
1454
- catch (err) {
1455
- process.stderr.write(`[patchwork] adapter init failed: ${err instanceof Error ? err.message : String(err)}\n`);
2622
+ // Skip the bridge-mode tail entirely when a subcommand IIFE will own the
2623
+ // process. `parseConfig` validates argv against the bridge's known-flag list
2624
+ // and raises "Unknown option" for subcommand-specific flags (e.g. `recipe
2625
+ // new --out .`); without this guard that throw kills the process before
2626
+ // the IIFE's microtask runs. The subcommand handles its own arg parsing.
2627
+ if (__subcommandWillRun) {
2628
+ // Subcommand IIFE is in flight or about to fire; sit tight until it
2629
+ // process.exits. Empty body — control naturally falls past end-of-file
2630
+ // and Node keeps the process alive on the IIFE's pending microtask.
1456
2631
  }
1457
- // If --analytics flag was passed, persist the preference immediately
1458
- if (config.analyticsEnabled !== null) {
1459
- setAnalyticsPref(config.analyticsEnabled);
1460
- }
1461
- // Auto-tmux: if requested and not already inside tmux or screen, re-exec inside a tmux session
1462
- if (config.autoTmux &&
1463
- !process.env.TMUX &&
1464
- !process.env.STY &&
1465
- !process.env.ZELLIJ &&
1466
- !process.env.ZELLIJ_SESSION_NAME) {
1467
- const ws = config.workspace.replace(/[^a-zA-Z0-9]/g, "").slice(-8);
1468
- const hash = crypto
1469
- .createHash("sha256")
1470
- .update(config.workspace)
1471
- .digest("hex")
1472
- .slice(0, 6);
1473
- const sessionName = `claude-bridge-${ws}${hash}`;
1474
- // Check if tmux is available
1475
- const tmuxCheck = spawnSync("which", ["tmux"], { stdio: "ignore" });
1476
- if (tmuxCheck.status !== 0) {
1477
- process.stderr.write("WARNING: --auto-tmux requested but tmux is not installed. Running without tmux.\n");
1478
- }
1479
- else {
1480
- // Strip --auto-tmux from argv to avoid infinite re-exec loop
1481
- const newArgv = process.argv.filter((a) => a !== "--auto-tmux");
1482
- // Pass each argv token as a separate tmux argument so paths with spaces work correctly
1483
- const result = spawnSync("tmux", ["new-session", "-d", "-s", sessionName, ...newArgv], { stdio: "inherit", timeout: 5000 });
1484
- if (result.status === 0) {
1485
- process.stderr.write(`Bridge launched in tmux session '${sessionName}'.\n`);
1486
- process.stderr.write(` Attach with: tmux attach -t ${sessionName}\n`);
1487
- process.exit(0);
1488
- }
1489
- else {
1490
- // tmux session likely already exists — attach to it or fall through
1491
- process.stderr.write(`WARNING: Could not create tmux session '${sessionName}' (already exists?). Running without auto-tmux.\n`);
2632
+ else {
2633
+ const config = parseConfig(process.argv);
2634
+ // Patchwork: resolve --model flag (optional, non-invasive) — stashes the
2635
+ // configured adapter on globalThis for consumers that opt into the adapter
2636
+ // layer. Bridge subprocess driver still works when --model is absent.
2637
+ try {
2638
+ const { resolveModel } = await import("./patchworkCli.js");
2639
+ const resolved = resolveModel(process.argv);
2640
+ if (resolved) {
2641
+ globalThis.__patchworkAdapter =
2642
+ resolved.adapter;
2643
+ process.stderr.write(`[patchwork] model adapter initialized: ${resolved.adapter.name}\n`);
1492
2644
  }
1493
2645
  }
1494
- }
1495
- // --watch: supervisor mode spawn this binary as a child (without --watch) and restart on crash
1496
- if (config.watch) {
1497
- const childArgv = process.argv.filter((a) => a !== "--watch");
1498
- const STABLE_THRESHOLD_MS = 60_000;
1499
- const BASE_DELAY_MS = 2_000;
1500
- const MAX_DELAY_MS = 30_000;
1501
- let delay = BASE_DELAY_MS;
1502
- let stopping = false;
1503
- function runChild() {
1504
- if (stopping)
1505
- return;
1506
- const startAt = Date.now();
1507
- process.stderr.write("[supervisor] starting bridge\n");
1508
- const [cmd, ...args] = childArgv;
1509
- if (!cmd)
1510
- return;
1511
- const child = spawn(cmd, args, {
1512
- stdio: "inherit",
1513
- });
1514
- for (const sig of ["SIGTERM", "SIGINT"]) {
1515
- process.once(sig, () => {
1516
- stopping = true;
1517
- child.kill(sig);
1518
- });
2646
+ catch (err) {
2647
+ process.stderr.write(`[patchwork] adapter init failed: ${err instanceof Error ? err.message : String(err)}\n`);
2648
+ }
2649
+ // If --analytics flag was passed, persist the preference immediately
2650
+ if (config.analyticsEnabled !== null) {
2651
+ setAnalyticsPref(config.analyticsEnabled);
2652
+ }
2653
+ // Auto-tmux: if requested and not already inside tmux or screen, re-exec inside a tmux session
2654
+ if (config.autoTmux &&
2655
+ !process.env.TMUX &&
2656
+ !process.env.STY &&
2657
+ !process.env.ZELLIJ &&
2658
+ !process.env.ZELLIJ_SESSION_NAME) {
2659
+ const ws = config.workspace.replace(/[^a-zA-Z0-9]/g, "").slice(-8);
2660
+ const hash = crypto
2661
+ .createHash("sha256")
2662
+ .update(config.workspace)
2663
+ .digest("hex")
2664
+ .slice(0, 6);
2665
+ const sessionName = `claude-bridge-${ws}${hash}`;
2666
+ // Check if tmux is available
2667
+ const tmuxCheck = spawnSync("which", ["tmux"], { stdio: "ignore" });
2668
+ if (tmuxCheck.status !== 0) {
2669
+ process.stderr.write("WARNING: --auto-tmux requested but tmux is not installed. Running without tmux.\n");
1519
2670
  }
1520
- child.on("exit", (code, signal) => {
1521
- if (stopping) {
1522
- process.stderr.write("[supervisor] bridge stopped\n");
2671
+ else {
2672
+ // Strip --auto-tmux from argv to avoid infinite re-exec loop
2673
+ const newArgv = process.argv.filter((a) => a !== "--auto-tmux");
2674
+ // Pass each argv token as a separate tmux argument so paths with spaces work correctly
2675
+ const result = spawnSync("tmux", ["new-session", "-d", "-s", sessionName, ...newArgv], { stdio: "inherit", timeout: 5000 });
2676
+ if (result.status === 0) {
2677
+ process.stderr.write(`Bridge launched in tmux session '${sessionName}'.\n`);
2678
+ process.stderr.write(` Attach with: tmux attach -t ${sessionName}\n`);
1523
2679
  process.exit(0);
1524
2680
  }
1525
- const uptime = Date.now() - startAt;
1526
- if (uptime >= STABLE_THRESHOLD_MS) {
1527
- delay = BASE_DELAY_MS; // reset backoff after a stable run
2681
+ else {
2682
+ // tmux session likely already exists — attach to it or fall through
2683
+ process.stderr.write(`WARNING: Could not create tmux session '${sessionName}' (already exists?). Running without auto-tmux.\n`);
1528
2684
  }
1529
- process.stderr.write(`[supervisor] bridge exited (code=${code ?? signal}), restarting in ${delay / 1000}s\n`);
1530
- setTimeout(() => {
1531
- delay = Math.min(delay * 2, MAX_DELAY_MS);
1532
- runChild();
1533
- }, delay);
1534
- });
2685
+ }
1535
2686
  }
1536
- runChild();
1537
- }
1538
- else {
1539
- const bridge = new Bridge(config);
1540
- bridge.start().catch((err) => {
1541
- const message = err instanceof Error ? err.message : String(err);
1542
- process.stderr.write(`Error: ${message}\n`);
1543
- process.exit(1);
1544
- });
1545
- // F5: Silent self-update nudge (fire-and-forget)
1546
- import("node:child_process")
1547
- .then(({ exec }) => {
1548
- exec("npm view claude-ide-bridge version", { timeout: 5000 }, (err, stdout) => {
1549
- if (err || !stdout)
2687
+ // Skip bridge boot when a subcommand IIFE is doing the work — avoids the
2688
+ // race where bridge.start() began initialising in parallel with the
2689
+ // subcommand's async path. See the KNOWN_SUBCOMMANDS / __subcommandWillRun
2690
+ // gate at the top of this file.
2691
+ if (__subcommandWillRun) {
2692
+ // intentionally empty subcommand IIFE owns the process from here.
2693
+ }
2694
+ // --watch: supervisor mode — spawn this binary as a child (without --watch) and restart on crash
2695
+ else if (config.watch) {
2696
+ const childArgv = process.argv.filter((a) => a !== "--watch");
2697
+ const STABLE_THRESHOLD_MS = 60_000;
2698
+ const BASE_DELAY_MS = 2_000;
2699
+ const MAX_DELAY_MS = 30_000;
2700
+ let delay = BASE_DELAY_MS;
2701
+ let stopping = false;
2702
+ function runChild() {
2703
+ if (stopping)
2704
+ return;
2705
+ const startAt = Date.now();
2706
+ process.stderr.write("[supervisor] starting bridge\n");
2707
+ const [cmd, ...args] = childArgv;
2708
+ if (!cmd)
1550
2709
  return;
1551
- const latest = stdout.trim();
1552
- if (latest && semverGt(latest, PACKAGE_VERSION)) {
1553
- console.log(`\n Bridge v${latest} available — run: npm update -g claude-ide-bridge\n`);
2710
+ const child = spawn(cmd, args, {
2711
+ stdio: "inherit",
2712
+ });
2713
+ for (const sig of ["SIGTERM", "SIGINT"]) {
2714
+ process.once(sig, () => {
2715
+ stopping = true;
2716
+ child.kill(sig);
2717
+ });
1554
2718
  }
2719
+ child.on("exit", (code, signal) => {
2720
+ if (stopping) {
2721
+ process.stderr.write("[supervisor] bridge stopped\n");
2722
+ process.exit(0);
2723
+ }
2724
+ const uptime = Date.now() - startAt;
2725
+ if (uptime >= STABLE_THRESHOLD_MS) {
2726
+ delay = BASE_DELAY_MS; // reset backoff after a stable run
2727
+ }
2728
+ process.stderr.write(`[supervisor] bridge exited (code=${code ?? signal}), restarting in ${delay / 1000}s\n`);
2729
+ setTimeout(() => {
2730
+ delay = Math.min(delay * 2, MAX_DELAY_MS);
2731
+ runChild();
2732
+ }, delay);
2733
+ });
2734
+ }
2735
+ runChild();
2736
+ }
2737
+ else {
2738
+ const bridge = new Bridge(config);
2739
+ bridge.start().catch((err) => {
2740
+ const message = err instanceof Error ? err.message : String(err);
2741
+ process.stderr.write(`Error: ${message}\n`);
2742
+ process.exit(1);
1555
2743
  });
1556
- })
1557
- .catch(() => { });
1558
- }
2744
+ // F5: Silent self-update nudge (fire-and-forget)
2745
+ import("node:child_process")
2746
+ .then(({ exec }) => {
2747
+ exec("npm view claude-ide-bridge version", { timeout: 5000 }, (err, stdout) => {
2748
+ if (err || !stdout)
2749
+ return;
2750
+ const latest = stdout.trim();
2751
+ if (latest && semverGt(latest, PACKAGE_VERSION)) {
2752
+ console.log(`\n Bridge v${latest} available — run: npm update -g claude-ide-bridge\n`);
2753
+ }
2754
+ });
2755
+ })
2756
+ .catch(() => { });
2757
+ }
2758
+ } // end of `else` for `if (__subcommandWillRun)` (bridge-mode block)
1559
2759
  //# sourceMappingURL=index.js.map