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

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