patchwork-os 0.2.0-alpha.34 → 0.2.0-alpha.36

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 (270) hide show
  1. package/README.md +202 -93
  2. package/deploy/bootstrap-new-vps.sh +12 -12
  3. package/deploy/bootstrap-vps.sh +6 -3
  4. package/deploy/deploy-landing.sh +59 -2
  5. package/dist/activityLog.d.ts +49 -0
  6. package/dist/activityLog.js +78 -0
  7. package/dist/activityLog.js.map +1 -1
  8. package/dist/approvalHttp.d.ts +25 -0
  9. package/dist/approvalHttp.js +74 -18
  10. package/dist/approvalHttp.js.map +1 -1
  11. package/dist/approvalInsights.d.ts +49 -0
  12. package/dist/approvalInsights.js +97 -0
  13. package/dist/approvalInsights.js.map +1 -0
  14. package/dist/approvalQueue.d.ts +11 -0
  15. package/dist/approvalQueue.js +80 -1
  16. package/dist/approvalQueue.js.map +1 -1
  17. package/dist/approvalSignals.d.ts +124 -0
  18. package/dist/approvalSignals.js +512 -0
  19. package/dist/approvalSignals.js.map +1 -0
  20. package/dist/automation.d.ts +37 -0
  21. package/dist/automation.js +105 -61
  22. package/dist/automation.js.map +1 -1
  23. package/dist/automationSuggestions.d.ts +79 -0
  24. package/dist/automationSuggestions.js +150 -0
  25. package/dist/automationSuggestions.js.map +1 -0
  26. package/dist/bridge.js +78 -1
  27. package/dist/bridge.js.map +1 -1
  28. package/dist/ccPermissions.d.ts +15 -0
  29. package/dist/ccPermissions.js +15 -0
  30. package/dist/ccPermissions.js.map +1 -1
  31. package/dist/claudeDriver.js +74 -16
  32. package/dist/claudeDriver.js.map +1 -1
  33. package/dist/commands/patchworkInit.d.ts +8 -0
  34. package/dist/commands/patchworkInit.js +41 -5
  35. package/dist/commands/patchworkInit.js.map +1 -1
  36. package/dist/commands/recipe.d.ts +20 -0
  37. package/dist/commands/recipe.js +212 -6
  38. package/dist/commands/recipe.js.map +1 -1
  39. package/dist/commands/recipeInstall.d.ts +79 -1
  40. package/dist/commands/recipeInstall.js +333 -16
  41. package/dist/commands/recipeInstall.js.map +1 -1
  42. package/dist/commands/tracesExport.d.ts +83 -0
  43. package/dist/commands/tracesExport.js +269 -0
  44. package/dist/commands/tracesExport.js.map +1 -0
  45. package/dist/commands/tracesImport.d.ts +56 -0
  46. package/dist/commands/tracesImport.js +161 -0
  47. package/dist/commands/tracesImport.js.map +1 -0
  48. package/dist/config.d.ts +8 -0
  49. package/dist/config.js +9 -1
  50. package/dist/config.js.map +1 -1
  51. package/dist/connectorRoutes.d.ts +43 -0
  52. package/dist/connectorRoutes.js +1023 -0
  53. package/dist/connectorRoutes.js.map +1 -0
  54. package/dist/connectors/asana.d.ts +198 -0
  55. package/dist/connectors/asana.js +679 -0
  56. package/dist/connectors/asana.js.map +1 -0
  57. package/dist/connectors/baseConnector.d.ts +36 -0
  58. package/dist/connectors/baseConnector.js +151 -28
  59. package/dist/connectors/baseConnector.js.map +1 -1
  60. package/dist/connectors/discord.d.ts +150 -0
  61. package/dist/connectors/discord.js +543 -0
  62. package/dist/connectors/discord.js.map +1 -0
  63. package/dist/connectors/github.js +11 -4
  64. package/dist/connectors/github.js.map +1 -1
  65. package/dist/connectors/gitlab.d.ts +180 -0
  66. package/dist/connectors/gitlab.js +582 -0
  67. package/dist/connectors/gitlab.js.map +1 -0
  68. package/dist/connectors/gmail.js +50 -10
  69. package/dist/connectors/gmail.js.map +1 -1
  70. package/dist/connectors/googleCalendar.js +36 -10
  71. package/dist/connectors/googleCalendar.js.map +1 -1
  72. package/dist/connectors/googleDrive.d.ts +34 -0
  73. package/dist/connectors/googleDrive.js +321 -0
  74. package/dist/connectors/googleDrive.js.map +1 -0
  75. package/dist/connectors/linear.js +23 -4
  76. package/dist/connectors/linear.js.map +1 -1
  77. package/dist/connectors/mcpOAuth.js +26 -2
  78. package/dist/connectors/mcpOAuth.js.map +1 -1
  79. package/dist/connectors/oauthStateStore.d.ts +31 -0
  80. package/dist/connectors/oauthStateStore.js +52 -0
  81. package/dist/connectors/oauthStateStore.js.map +1 -0
  82. package/dist/connectors/pagerduty.d.ts +160 -0
  83. package/dist/connectors/pagerduty.js +464 -0
  84. package/dist/connectors/pagerduty.js.map +1 -0
  85. package/dist/connectors/slack.d.ts +16 -1
  86. package/dist/connectors/slack.js +57 -5
  87. package/dist/connectors/slack.js.map +1 -1
  88. package/dist/connectors/tokenStorage.js +27 -2
  89. package/dist/connectors/tokenStorage.js.map +1 -1
  90. package/dist/connectors/zendesk.js +19 -1
  91. package/dist/connectors/zendesk.js.map +1 -1
  92. package/dist/cors.d.ts +10 -0
  93. package/dist/cors.js +29 -0
  94. package/dist/cors.js.map +1 -0
  95. package/dist/decisionReplay.d.ts +72 -0
  96. package/dist/decisionReplay.js +92 -0
  97. package/dist/decisionReplay.js.map +1 -0
  98. package/dist/decisionTraceLog.d.ts +6 -0
  99. package/dist/decisionTraceLog.js +54 -2
  100. package/dist/decisionTraceLog.js.map +1 -1
  101. package/dist/featureFlags.d.ts +17 -11
  102. package/dist/featureFlags.js +52 -47
  103. package/dist/featureFlags.js.map +1 -1
  104. package/dist/fp/automationInterpreter.js +25 -21
  105. package/dist/fp/automationInterpreter.js.map +1 -1
  106. package/dist/fp/automationState.js +4 -1
  107. package/dist/fp/automationState.js.map +1 -1
  108. package/dist/fp/policyParser.js +4 -1
  109. package/dist/fp/policyParser.js.map +1 -1
  110. package/dist/inboxRoutes.d.ts +22 -0
  111. package/dist/inboxRoutes.js +114 -0
  112. package/dist/inboxRoutes.js.map +1 -0
  113. package/dist/index.js +734 -144
  114. package/dist/index.js.map +1 -1
  115. package/dist/mcpRoutes.d.ts +37 -0
  116. package/dist/mcpRoutes.js +76 -0
  117. package/dist/mcpRoutes.js.map +1 -0
  118. package/dist/oauth.d.ts +3 -0
  119. package/dist/oauth.js +151 -26
  120. package/dist/oauth.js.map +1 -1
  121. package/dist/oauthRoutes.d.ts +32 -0
  122. package/dist/oauthRoutes.js +124 -0
  123. package/dist/oauthRoutes.js.map +1 -0
  124. package/dist/orchestrator/orchestratorBridge.js +2 -2
  125. package/dist/orchestrator/orchestratorBridge.js.map +1 -1
  126. package/dist/patchworkConfig.d.ts +7 -0
  127. package/dist/patchworkConfig.js.map +1 -1
  128. package/dist/pluginLoader.d.ts +12 -0
  129. package/dist/pluginLoader.js +43 -4
  130. package/dist/pluginLoader.js.map +1 -1
  131. package/dist/pluginWatcher.js +8 -3
  132. package/dist/pluginWatcher.js.map +1 -1
  133. package/dist/preToolUseHook.d.ts +12 -0
  134. package/dist/preToolUseHook.js +23 -0
  135. package/dist/preToolUseHook.js.map +1 -1
  136. package/dist/recipeOrchestration.d.ts +8 -0
  137. package/dist/recipeOrchestration.js +320 -39
  138. package/dist/recipeOrchestration.js.map +1 -1
  139. package/dist/recipeRoutes.d.ts +154 -0
  140. package/dist/recipeRoutes.js +1098 -0
  141. package/dist/recipeRoutes.js.map +1 -0
  142. package/dist/recipes/captureForRunlog.d.ts +27 -0
  143. package/dist/recipes/captureForRunlog.js +128 -0
  144. package/dist/recipes/captureForRunlog.js.map +1 -0
  145. package/dist/recipes/chainedRunner.d.ts +54 -3
  146. package/dist/recipes/chainedRunner.js +256 -36
  147. package/dist/recipes/chainedRunner.js.map +1 -1
  148. package/dist/recipes/compiler.js +3 -3
  149. package/dist/recipes/compiler.js.map +1 -1
  150. package/dist/recipes/detectSilentFail.d.ts +34 -0
  151. package/dist/recipes/detectSilentFail.js +105 -0
  152. package/dist/recipes/detectSilentFail.js.map +1 -0
  153. package/dist/recipes/installer.js +3 -3
  154. package/dist/recipes/installer.js.map +1 -1
  155. package/dist/recipes/manifest.js +21 -6
  156. package/dist/recipes/manifest.js.map +1 -1
  157. package/dist/recipes/migrationWarnings.d.ts +12 -0
  158. package/dist/recipes/migrationWarnings.js +44 -0
  159. package/dist/recipes/migrationWarnings.js.map +1 -0
  160. package/dist/recipes/replayRun.d.ts +62 -0
  161. package/dist/recipes/replayRun.js +97 -0
  162. package/dist/recipes/replayRun.js.map +1 -0
  163. package/dist/recipes/resolveRecipePath.d.ts +69 -0
  164. package/dist/recipes/resolveRecipePath.js +202 -0
  165. package/dist/recipes/resolveRecipePath.js.map +1 -0
  166. package/dist/recipes/scheduler.js +102 -11
  167. package/dist/recipes/scheduler.js.map +1 -1
  168. package/dist/recipes/schemaGenerator.js +3 -3
  169. package/dist/recipes/schemaGenerator.js.map +1 -1
  170. package/dist/recipes/toolRegistry.d.ts +5 -0
  171. package/dist/recipes/toolRegistry.js +9 -0
  172. package/dist/recipes/toolRegistry.js.map +1 -1
  173. package/dist/recipes/tools/asana.d.ts +16 -0
  174. package/dist/recipes/tools/asana.js +524 -0
  175. package/dist/recipes/tools/asana.js.map +1 -0
  176. package/dist/recipes/tools/discord.d.ts +18 -0
  177. package/dist/recipes/tools/discord.js +254 -0
  178. package/dist/recipes/tools/discord.js.map +1 -0
  179. package/dist/recipes/tools/file.d.ts +6 -0
  180. package/dist/recipes/tools/file.js +12 -8
  181. package/dist/recipes/tools/file.js.map +1 -1
  182. package/dist/recipes/tools/github.js +29 -4
  183. package/dist/recipes/tools/github.js.map +1 -1
  184. package/dist/recipes/tools/gitlab.d.ts +11 -0
  185. package/dist/recipes/tools/gitlab.js +285 -0
  186. package/dist/recipes/tools/gitlab.js.map +1 -0
  187. package/dist/recipes/tools/gmail.d.ts +1 -1
  188. package/dist/recipes/tools/gmail.js +230 -6
  189. package/dist/recipes/tools/gmail.js.map +1 -1
  190. package/dist/recipes/tools/googleDrive.d.ts +1 -0
  191. package/dist/recipes/tools/googleDrive.js +55 -0
  192. package/dist/recipes/tools/googleDrive.js.map +1 -0
  193. package/dist/recipes/tools/index.d.ts +8 -0
  194. package/dist/recipes/tools/index.js +8 -0
  195. package/dist/recipes/tools/index.js.map +1 -1
  196. package/dist/recipes/tools/jira.d.ts +14 -0
  197. package/dist/recipes/tools/jira.js +369 -0
  198. package/dist/recipes/tools/jira.js.map +1 -0
  199. package/dist/recipes/tools/linear.d.ts +2 -1
  200. package/dist/recipes/tools/linear.js +227 -3
  201. package/dist/recipes/tools/linear.js.map +1 -1
  202. package/dist/recipes/tools/meetingNotes.d.ts +21 -0
  203. package/dist/recipes/tools/meetingNotes.js +701 -0
  204. package/dist/recipes/tools/meetingNotes.js.map +1 -0
  205. package/dist/recipes/tools/pagerduty.d.ts +15 -0
  206. package/dist/recipes/tools/pagerduty.js +451 -0
  207. package/dist/recipes/tools/pagerduty.js.map +1 -0
  208. package/dist/recipes/tools/sentry.d.ts +12 -0
  209. package/dist/recipes/tools/sentry.js +73 -0
  210. package/dist/recipes/tools/sentry.js.map +1 -0
  211. package/dist/recipes/tools/slack.js +15 -5
  212. package/dist/recipes/tools/slack.js.map +1 -1
  213. package/dist/recipes/validation.js +83 -14
  214. package/dist/recipes/validation.js.map +1 -1
  215. package/dist/recipes/yamlRunner.d.ts +30 -2
  216. package/dist/recipes/yamlRunner.js +369 -70
  217. package/dist/recipes/yamlRunner.js.map +1 -1
  218. package/dist/recipesHttp.d.ts +76 -1
  219. package/dist/recipesHttp.js +474 -12
  220. package/dist/recipesHttp.js.map +1 -1
  221. package/dist/runLog.d.ts +78 -2
  222. package/dist/runLog.js +204 -6
  223. package/dist/runLog.js.map +1 -1
  224. package/dist/schemas/dry-run-plan.v1.json +139 -0
  225. package/dist/schemas/recipe.v1.json +684 -0
  226. package/dist/server.d.ts +79 -10
  227. package/dist/server.js +366 -1384
  228. package/dist/server.js.map +1 -1
  229. package/dist/ssrfGuard.d.ts +54 -0
  230. package/dist/ssrfGuard.js +122 -0
  231. package/dist/ssrfGuard.js.map +1 -0
  232. package/dist/streamableHttp.d.ts +39 -1
  233. package/dist/streamableHttp.js +126 -17
  234. package/dist/streamableHttp.js.map +1 -1
  235. package/dist/tools/getDocumentSymbols.d.ts +24 -0
  236. package/dist/tools/getDocumentSymbols.js +74 -8
  237. package/dist/tools/getDocumentSymbols.js.map +1 -1
  238. package/dist/tools/getSecurityAdvisories.js +10 -1
  239. package/dist/tools/getSecurityAdvisories.js.map +1 -1
  240. package/dist/tools/getSessionUsage.d.ts +3 -0
  241. package/dist/tools/getSessionUsage.js +3 -0
  242. package/dist/tools/getSessionUsage.js.map +1 -1
  243. package/dist/tools/index.d.ts +8 -0
  244. package/dist/tools/index.js +32 -2
  245. package/dist/tools/index.js.map +1 -1
  246. package/dist/tools/slackPostMessage.js +1 -1
  247. package/dist/tools/slackPostMessage.js.map +1 -1
  248. package/dist/tools/transaction.d.ts +19 -0
  249. package/dist/tools/transaction.js +29 -0
  250. package/dist/tools/transaction.js.map +1 -1
  251. package/dist/traceEncryption.d.ts +46 -0
  252. package/dist/traceEncryption.js +124 -0
  253. package/dist/traceEncryption.js.map +1 -0
  254. package/dist/transport.d.ts +39 -0
  255. package/dist/transport.js +88 -8
  256. package/dist/transport.js.map +1 -1
  257. package/package.json +22 -5
  258. package/templates/policies/README.md +72 -0
  259. package/templates/policies/conservative.json +14 -0
  260. package/templates/policies/developer.json +14 -0
  261. package/templates/policies/headless-ci.json +24 -0
  262. package/templates/policies/personal-assistant.json +15 -0
  263. package/templates/policies/regulated-industry.json +18 -0
  264. package/templates/recipes/project-health-check.yaml +1 -1
  265. package/templates/recipes/webhook/README.md +70 -0
  266. package/templates/recipes/webhook/capture-thought.yaml +26 -0
  267. package/templates/recipes/webhook/customer-escalation.yaml +49 -0
  268. package/templates/recipes/webhook/incident-intake.yaml +46 -0
  269. package/templates/recipes/webhook/meeting-prep.yaml +48 -0
  270. package/templates/recipes/webhook/morning-brief.yaml +57 -0
package/dist/index.js CHANGED
@@ -92,6 +92,61 @@ async function downloadVsixFromOpenVsx() {
92
92
  writeFileSync(tmpPath, Buffer.from(buf));
93
93
  return tmpPath;
94
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
+ ];
131
+ const __invokedSubcommand = (() => {
132
+ const sub = process.argv[2];
133
+ if (!sub || sub.startsWith("-"))
134
+ return null;
135
+ // Treat KNOWN_SUBCOMMANDS as the dispatch source. The bare-binary
136
+ // dashboard launcher (no argv) is handled separately below.
137
+ return KNOWN_SUBCOMMANDS.includes(sub)
138
+ ? sub
139
+ : null;
140
+ })();
141
+ const __invokedBareBinaryDashboard = (() => {
142
+ if (process.argv[2])
143
+ return false;
144
+ const binName = path.basename(process.argv[1] ?? "");
145
+ return (binName === "patchwork-os" ||
146
+ binName === "patchwork" ||
147
+ binName === "patchwork.js");
148
+ })();
149
+ const __subcommandWillRun = __invokedSubcommand !== null || __invokedBareBinaryDashboard;
95
150
  // Handle --version flag — print package version and exit.
96
151
  if (process.argv[2] === "--version" || process.argv[2] === "-v") {
97
152
  console.log(`claude-ide-bridge ${PACKAGE_VERSION}`);
@@ -489,14 +544,20 @@ Options:
489
544
  // Handle gen-plugin-stub subcommand — scaffolds a new plugin directory
490
545
  if (process.argv[2] === "gen-plugin-stub") {
491
546
  const argv = process.argv.slice(3);
492
- // Parse args: gen-plugin-stub <dir> [--name <name>] [--prefix <prefix>]
547
+ // Parse args: gen-plugin-stub <dir> [--name <name>] [--prefix <prefix>] [--ts]
493
548
  const dirArg = argv.find((a) => !a.startsWith("--"));
494
549
  if (!dirArg) {
495
- process.stderr.write("Usage: claude-ide-bridge gen-plugin-stub <output-dir> [--name <org/plugin-name>] [--prefix <toolPrefix>]\n");
550
+ process.stderr.write("Usage: claude-ide-bridge gen-plugin-stub <output-dir> [--name <org/plugin-name>] [--prefix <toolPrefix>] [--ts]\n");
496
551
  process.exit(1);
497
552
  }
498
553
  const nameIdx = argv.indexOf("--name");
499
554
  const prefixIdx = argv.indexOf("--prefix");
555
+ // --ts emits a TypeScript variant (src/index.ts + tsconfig.json + build
556
+ // scripts) alongside a compiled-output manifest pointing at index.mjs.
557
+ // Plugin authors get type-checked tools without changing the hot-reload
558
+ // contract — `npm run dev` watches src/, emits index.mjs, bridge picks
559
+ // up the rebuilt artifact via --plugin-watch.
560
+ const useTypeScript = argv.includes("--ts");
500
561
  const pluginName = nameIdx !== -1 && argv[nameIdx + 1]
501
562
  ? argv[nameIdx + 1]
502
563
  : "my-org/my-plugin";
@@ -530,8 +591,8 @@ if (process.argv[2] === "gen-plugin-stub") {
530
591
  minBridgeVersion: "2.1.24",
531
592
  };
532
593
  writeFileSync(path.join(outDir, "claude-ide-bridge-plugin.json"), `${JSON.stringify(manifest, null, 2)}\n`, "utf-8");
533
- // index.mjs entrypoint
534
- const entrypoint = `/**
594
+ // ── shared tool body — same logic, different surface syntax ──
595
+ const jsEntrypoint = `/**
535
596
  * ${pluginName} — Claude IDE Bridge plugin
536
597
  *
537
598
  * Each tool must have a name starting with "${toolPrefix}".
@@ -539,7 +600,7 @@ if (process.argv[2] === "gen-plugin-stub") {
539
600
  * ctx.config (commandTimeout, maxResultSize), and ctx.logger.
540
601
  */
541
602
 
542
- /** @param {import('claude-ide-bridge/plugin').PluginContext} ctx */
603
+ /** @param {import('patchwork-os/plugin').PluginContext} ctx */
543
604
  export function register(ctx) {
544
605
  ctx.logger.info(${JSON.stringify(`${pluginName} loaded`)}, { workspace: ctx.workspace });
545
606
 
@@ -567,25 +628,216 @@ export function register(ctx) {
567
628
  };
568
629
  }
569
630
  `;
570
- writeFileSync(path.join(outDir, "index.mjs"), entrypoint, "utf-8");
571
- // package.json (optional, for npm publishing)
572
- const pkg = {
631
+ const tsEntrypoint = `/**
632
+ * ${pluginName} Claude IDE Bridge plugin
633
+ *
634
+ * Each tool must have a name starting with "${toolPrefix}".
635
+ * The \`ctx\` object provides: ctx.workspace, ctx.workspaceFolders,
636
+ * ctx.config (commandTimeout, maxResultSize), and ctx.logger.
637
+ */
638
+ import type { PluginContext } from "patchwork-os/plugin";
639
+
640
+ interface HelloArgs {
641
+ name: string;
642
+ }
643
+
644
+ export function register(ctx: PluginContext) {
645
+ ctx.logger.info(${JSON.stringify(`${pluginName} loaded`)}, { workspace: ctx.workspace });
646
+
647
+ return {
648
+ tools: [
649
+ {
650
+ schema: {
651
+ name: ${JSON.stringify(`${toolPrefix}Hello`)},
652
+ description: "Example tool — returns a greeting",
653
+ inputSchema: {
654
+ type: "object" as const,
655
+ required: ["name"] as const,
656
+ additionalProperties: false as const,
657
+ properties: {
658
+ name: { type: "string" as const, description: "Name to greet" },
659
+ },
660
+ },
661
+ annotations: { readOnlyHint: true },
662
+ },
663
+ handler: async (args: HelloArgs) => ({
664
+ content: [
665
+ {
666
+ type: "text" as const,
667
+ text: \`Hello from ${pluginName}, \${args.name}!\`,
668
+ },
669
+ ],
670
+ }),
671
+ },
672
+ ],
673
+ };
674
+ }
675
+ `;
676
+ // Write entrypoint — TS goes under src/, JS at root.
677
+ if (useTypeScript) {
678
+ mkdirSync(path.join(outDir, "src"), { recursive: true });
679
+ writeFileSync(path.join(outDir, "src", "index.ts"), tsEntrypoint, "utf-8");
680
+ }
681
+ else {
682
+ writeFileSync(path.join(outDir, "index.mjs"), jsEntrypoint, "utf-8");
683
+ }
684
+ // tsconfig.json — TS variant only. Emits a single ESM file at index.mjs
685
+ // so the plugin manifest's entrypoint stays the same shape as the JS
686
+ // scaffold and --plugin-watch reload semantics don't change.
687
+ if (useTypeScript) {
688
+ const tsconfig = {
689
+ compilerOptions: {
690
+ target: "ES2022",
691
+ module: "ES2022",
692
+ moduleResolution: "Bundler",
693
+ outDir: ".",
694
+ rootDir: "src",
695
+ declaration: false,
696
+ strict: true,
697
+ esModuleInterop: true,
698
+ skipLibCheck: true,
699
+ resolveJsonModule: true,
700
+ // Emit .mjs so the plugin loader (which expects ESM) picks it up
701
+ // without relying on package.json "type": "module" alone.
702
+ // tsc doesn't emit .mjs natively, so package.json's "build" script
703
+ // does a rename pass — see below.
704
+ },
705
+ include: ["src/**/*"],
706
+ exclude: ["node_modules"],
707
+ };
708
+ writeFileSync(path.join(outDir, "tsconfig.json"), `${JSON.stringify(tsconfig, null, 2)}\n`, "utf-8");
709
+ }
710
+ // package.json — TS variant adds build + dev (watch) scripts.
711
+ const pkgBase = {
573
712
  name: pluginName.replace(/^@[^/]+\//, "").replace(/\//g, "-"),
574
713
  version: "0.1.0",
575
714
  description: "A Claude IDE Bridge plugin",
576
715
  type: "module",
577
716
  main: "index.mjs",
578
- keywords: ["claude-ide-bridge", "claude-ide-bridge-plugin"],
579
- peerDependencies: { "claude-ide-bridge": ">=2.1.24" },
717
+ keywords: ["patchwork-os", "claude-ide-bridge-plugin"],
718
+ peerDependencies: { "patchwork-os": ">=0.2.0-alpha.0" },
580
719
  };
720
+ const pkg = useTypeScript
721
+ ? {
722
+ ...pkgBase,
723
+ scripts: {
724
+ // tsc emits index.js — rename to index.mjs so the loader treats
725
+ // it as ESM regardless of the consumer's package.json.
726
+ build: "tsc && mv index.js index.mjs",
727
+ dev: "tsc --watch",
728
+ clean: "rm -f index.mjs",
729
+ },
730
+ devDependencies: {
731
+ typescript: "^5.4.0",
732
+ "patchwork-os": ">=0.2.0-alpha.0",
733
+ },
734
+ }
735
+ : pkgBase;
581
736
  writeFileSync(path.join(outDir, "package.json"), `${JSON.stringify(pkg, null, 2)}\n`, "utf-8");
737
+ // README.md — included in both variants. Spells out the hot-reload
738
+ // contract so plugin authors don't have to read the platform docs to
739
+ // get started.
740
+ const readmeBody = useTypeScript
741
+ ? `# ${pluginName}
742
+
743
+ A [Claude IDE Bridge](https://github.com/Oolab-labs/patchwork-os) plugin (TypeScript).
744
+
745
+ ## Quick start
746
+
747
+ \`\`\`sh
748
+ npm install
749
+ npm run dev # in one terminal — watches src/, emits index.mjs
750
+
751
+ # In another terminal:
752
+ claude-ide-bridge --plugin . --plugin-watch
753
+ \`\`\`
754
+
755
+ Edit \`src/index.ts\`. \`tsc --watch\` rebuilds, the bridge hot-reloads, your tool is callable from the live Claude session on the next turn.
756
+
757
+ ## Build for distribution
758
+
759
+ \`\`\`sh
760
+ npm run build # emits index.mjs
761
+ npm publish # publish to npm (optional)
762
+ \`\`\`
763
+
764
+ When published with the \`claude-ide-bridge-plugin\` keyword, users can install with:
765
+
766
+ \`\`\`sh
767
+ claude-ide-bridge --plugin ${pluginName.replace(/^@[^/]+\//, "")}
768
+ \`\`\`
769
+
770
+ ## Tool naming
771
+
772
+ 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}$/\`).
773
+
774
+ ## Plugin context
775
+
776
+ The \`ctx\` argument to \`register()\` provides:
777
+
778
+ - \`ctx.workspace\` — workspace root path
779
+ - \`ctx.workspaceFolders\` — array of workspace folders
780
+ - \`ctx.config\` — \`{ commandTimeout, maxResultSize }\`
781
+ - \`ctx.logger\` — \`info\` / \`warn\` / \`error\` logging that respects bridge log level
782
+
783
+ ## Live toolsmithing
784
+
785
+ 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.
786
+
787
+ See [documents/live-toolsmithing.md](https://github.com/Oolab-labs/patchwork-os/blob/main/documents/live-toolsmithing.md) for the full narrative.
788
+ `
789
+ : `# ${pluginName}
790
+
791
+ A [Claude IDE Bridge](https://github.com/Oolab-labs/patchwork-os) plugin.
792
+
793
+ ## Quick start
794
+
795
+ \`\`\`sh
796
+ claude-ide-bridge --plugin . --plugin-watch
797
+ \`\`\`
798
+
799
+ 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.
800
+
801
+ ## Tool naming
802
+
803
+ 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}$/\`).
804
+
805
+ ## Plugin context
806
+
807
+ The \`ctx\` argument to \`register()\` provides:
808
+
809
+ - \`ctx.workspace\` — workspace root path
810
+ - \`ctx.workspaceFolders\` — array of workspace folders
811
+ - \`ctx.config\` — \`{ commandTimeout, maxResultSize }\`
812
+ - \`ctx.logger\` — \`info\` / \`warn\` / \`error\` logging that respects bridge log level
813
+
814
+ ## Want types?
815
+
816
+ Re-scaffold with \`claude-ide-bridge gen-plugin-stub <dir> --ts\` for a TypeScript variant with \`tsc --watch\` build pipeline.
817
+
818
+ ## Live toolsmithing
819
+
820
+ 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.
821
+ `;
822
+ writeFileSync(path.join(outDir, "README.md"), readmeBody, "utf-8");
582
823
  // .gitignore
583
- writeFileSync(path.join(outDir, ".gitignore"), "node_modules\n", "utf-8");
584
- process.stderr.write(`✓ Plugin stub created at ${outDir}\n`);
824
+ const gitignore = useTypeScript
825
+ ? "node_modules\nindex.mjs\nindex.js\n*.tsbuildinfo\n"
826
+ : "node_modules\n";
827
+ writeFileSync(path.join(outDir, ".gitignore"), gitignore, "utf-8");
828
+ process.stderr.write(`✓ Plugin stub created at ${outDir} (${useTypeScript ? "TypeScript" : "JavaScript"})\n`);
585
829
  process.stderr.write("\nNext steps:\n");
586
- process.stderr.write(` 1. Edit ${path.join(outDir, "index.mjs")} to implement your tools\n`);
587
- process.stderr.write(` 2. Run the bridge with: claude-ide-bridge --plugin ${outDir}\n`);
588
- process.stderr.write(` 3. Or add to your config: { "plugins": ["${outDir}"] }\n`);
830
+ if (useTypeScript) {
831
+ process.stderr.write(` 1. cd ${outDir} && npm install\n`);
832
+ process.stderr.write(` 2. Edit ${path.join(outDir, "src", "index.ts")} to implement your tools\n`);
833
+ process.stderr.write(` 3. npm run dev (in one terminal)\n`);
834
+ process.stderr.write(` 4. claude-ide-bridge --plugin ${outDir} --plugin-watch (in another)\n`);
835
+ }
836
+ else {
837
+ process.stderr.write(` 1. Edit ${path.join(outDir, "index.mjs")} to implement your tools\n`);
838
+ process.stderr.write(` 2. Run the bridge with: claude-ide-bridge --plugin ${outDir} --plugin-watch\n`);
839
+ process.stderr.write(` 3. Or add to your config: { "plugins": ["${outDir}"] }\n`);
840
+ }
589
841
  process.exit(0);
590
842
  }
591
843
  // Patchwork: `patchwork recipe list` — enumerate installed recipes.
@@ -597,6 +849,68 @@ if (process.argv[2] === "recipe" && process.argv[3] === "list") {
597
849
  process.exit(0);
598
850
  })();
599
851
  }
852
+ // Patchwork: `patchwork recipe enable <name>` / `recipe disable <name>` —
853
+ // flip the disabled marker so scheduled triggers (cron/file-watch) take
854
+ // effect (or stop). Manual `recipe run` is unaffected.
855
+ if (process.argv[2] === "recipe" &&
856
+ (process.argv[3] === "enable" || process.argv[3] === "disable")) {
857
+ const subcommand = process.argv[3];
858
+ const name = process.argv[4];
859
+ if (!name) {
860
+ process.stderr.write(`Usage: patchwork recipe ${subcommand} <name>\n` +
861
+ ` See \`patchwork recipe list\` for installed recipe names.\n`);
862
+ process.exit(1);
863
+ }
864
+ (async () => {
865
+ try {
866
+ const { runRecipeEnable, runRecipeDisable } = await import("./commands/recipeInstall.js");
867
+ if (subcommand === "enable") {
868
+ const r = runRecipeEnable(name);
869
+ process.stdout.write(r.alreadyEnabled
870
+ ? ` ℹ ${r.name} is already enabled\n`
871
+ : ` ✓ enabled ${r.name}\n`);
872
+ }
873
+ else {
874
+ const r = runRecipeDisable(name);
875
+ process.stdout.write(r.alreadyDisabled
876
+ ? ` ℹ ${r.name} is already disabled\n`
877
+ : ` ✓ disabled ${r.name}\n`);
878
+ }
879
+ process.exit(0);
880
+ }
881
+ catch (err) {
882
+ process.stderr.write(`Error: ${err instanceof Error ? err.message : String(err)}\n`);
883
+ process.exit(1);
884
+ }
885
+ })();
886
+ }
887
+ // Patchwork: `patchwork recipe uninstall <name>` — remove an installed recipe
888
+ // directory and all its files. Sister to `recipe install`. Idempotent on
889
+ // success (subsequent uninstalls error with "no installed recipe").
890
+ if (process.argv[2] === "recipe" && process.argv[3] === "uninstall") {
891
+ const name = process.argv[4];
892
+ if (!name) {
893
+ process.stderr.write("Usage: patchwork recipe uninstall <name>\n" +
894
+ " See `patchwork recipe list` for installed recipe names.\n");
895
+ process.exit(1);
896
+ }
897
+ (async () => {
898
+ try {
899
+ const { runRecipeUninstall } = await import("./commands/recipeInstall.js");
900
+ const r = runRecipeUninstall(name);
901
+ if (!r.ok) {
902
+ process.stderr.write(`Error: ${r.error}\n`);
903
+ process.exit(1);
904
+ }
905
+ process.stdout.write(` ✓ Uninstalled ${name} (${r.installDir})\n`);
906
+ process.exit(0);
907
+ }
908
+ catch (err) {
909
+ process.stderr.write(`Error: ${err instanceof Error ? err.message : String(err)}\n`);
910
+ process.exit(1);
911
+ }
912
+ })();
913
+ }
600
914
  // Patchwork: `patchwork recipe run <name>` — runs a recipe locally or via
601
915
  // a running bridge's /recipes/run endpoint if one is available.
602
916
  if (process.argv[2] === "recipe" && process.argv[3] === "run") {
@@ -794,9 +1108,15 @@ if (process.argv[2] === "recipe" && process.argv[3] === "install") {
794
1108
  const result = installRecipeFromFile(path.resolve(source), {
795
1109
  recipesDir,
796
1110
  });
1111
+ // alpha.36+ — sidecar `<name>.permissions.json` is no longer written
1112
+ // (was decorative, never read by toolRegistry). Print the suggested
1113
+ // permissions snippet inline so users can hand-merge into settings.
797
1114
  process.stdout.write(` ✓ ${result.action} ${result.installedPath}\n` +
798
- ` ℹ permissions snippet written to ${result.installedPath}.permissions.json\n` +
799
- ` Review + merge into ~/.claude/settings.json to pre-approve recipe steps.\n`);
1115
+ ` ℹ Patchwork does not enforce per-recipe permissions; configure tool gating in ~/.claude/settings.json.\n` +
1116
+ ` Suggested permissions snippet:\n${result.permissionsJson
1117
+ .split("\n")
1118
+ .map((l) => ` ${l}`)
1119
+ .join("\n")}\n`);
800
1120
  }
801
1121
  else {
802
1122
  // Marketplace install: github:, https://, ./local/
@@ -812,7 +1132,264 @@ if (process.argv[2] === "recipe" && process.argv[3] === "install") {
812
1132
  }
813
1133
  })();
814
1134
  }
1135
+ // Patchwork: `patchwork suggest [--since <date>]` — pattern-mine the
1136
+ // activity log + run history for "you've been doing X by hand; want to
1137
+ // make a recipe?" hints. See documents/strategic/2026-05-02/memory-
1138
+ // ecosystem-report.md §6 for the catalog this implements.
1139
+ //
1140
+ // Three suggestion kinds: co-occurring tool pairs (worth a recipe), tools
1141
+ // installed but unused (worth reviewing or pruning), and recipes that
1142
+ // always succeed (worth trust-graduating). Read-only — does not change
1143
+ // any policy or registry state.
1144
+ if (process.argv[2] === "suggest") {
1145
+ (async () => {
1146
+ try {
1147
+ const args = process.argv.slice(3);
1148
+ let sinceDays;
1149
+ for (let i = 0; i < args.length; i++) {
1150
+ const a = args[i];
1151
+ if (a === "--since-days") {
1152
+ const next = args[i + 1];
1153
+ if (next)
1154
+ sinceDays = Number.parseInt(next, 10);
1155
+ i++;
1156
+ }
1157
+ else if (a === "--help" || a === "-h") {
1158
+ process.stdout.write("patchwork suggest [--since-days <N>]\n\n" +
1159
+ "Pattern-mine the activity log + recipe runs for automation hints:\n" +
1160
+ " - Co-occurring tool pairs that don't yet appear in any recipe\n" +
1161
+ " - Installed tools that haven't been called recently\n" +
1162
+ " - Recipes that have succeeded ≥ 10 times in a row (trust-graduation candidates)\n\n" +
1163
+ "Default lookback window is 7 days. --since-days overrides.\n\n" +
1164
+ "Read-only — does not modify policy, registry, or run history.\n");
1165
+ process.exit(0);
1166
+ }
1167
+ }
1168
+ const { ActivityLog } = await import("./activityLog.js");
1169
+ const { RecipeRunLog } = await import("./runLog.js");
1170
+ const { computeAutomationSuggestions } = await import("./automationSuggestions.js");
1171
+ // Side-effect import — populates the tool registry that
1172
+ // computeAutomationSuggestions consults for installed-tool inventory.
1173
+ await import("./recipes/tools/index.js");
1174
+ // Wire up the bridge's standard log paths. The CLI reads from
1175
+ // disk; it doesn't need a running bridge.
1176
+ const patchworkDir = path.join(os.homedir(), ".patchwork");
1177
+ const activityLog = new ActivityLog();
1178
+ // Find the most recent activity log file (any port). For the
1179
+ // suggest CLI we union all of them.
1180
+ const claudeIdeDir = path.join(os.homedir(), ".claude", "ide");
1181
+ try {
1182
+ const entries = await import("node:fs").then((m) => m.readdirSync(claudeIdeDir));
1183
+ for (const name of entries) {
1184
+ if (/^activity(-\d+)?\.jsonl$/i.test(name)) {
1185
+ activityLog.setPersistPath(path.join(claudeIdeDir, name));
1186
+ break; // setPersistPath loads on call; first existing wins
1187
+ }
1188
+ }
1189
+ }
1190
+ catch {
1191
+ // No activity dir / files — proceed with an empty log; the
1192
+ // suggestions just return fewer / no items.
1193
+ }
1194
+ const recipeRunLog = new RecipeRunLog({ dir: patchworkDir });
1195
+ const opts = {
1196
+ activityLog,
1197
+ recipeRunLog,
1198
+ };
1199
+ if (sinceDays !== undefined && Number.isFinite(sinceDays)) {
1200
+ opts.activitySinceMs = sinceDays * 24 * 60 * 60 * 1000;
1201
+ }
1202
+ const suggestions = computeAutomationSuggestions(opts);
1203
+ if (suggestions.length === 0) {
1204
+ process.stdout.write("No automation suggestions yet. Patchwork mines patterns from the activity log\n" +
1205
+ "and recipe run history; come back after a few days of use.\n");
1206
+ process.exit(0);
1207
+ }
1208
+ process.stdout.write(`${suggestions.length} suggestion${suggestions.length === 1 ? "" : "s"}:\n\n`);
1209
+ for (const s of suggestions) {
1210
+ const icon = s.kind === "co_occurring_pair"
1211
+ ? "→"
1212
+ : s.kind === "installed_but_unused"
1213
+ ? "·"
1214
+ : "★";
1215
+ process.stdout.write(` ${icon} ${s.label}\n`);
1216
+ }
1217
+ process.stdout.write("\nRead-only output. Nothing changed.\n");
1218
+ process.exit(0);
1219
+ }
1220
+ catch (err) {
1221
+ process.stderr.write(`Error: ${err instanceof Error ? err.message : String(err)}\n`);
1222
+ process.exit(1);
1223
+ }
1224
+ })();
1225
+ }
815
1226
  // Patchwork: `patchwork recipe schema [outputDir]` — write generated recipe schemas to disk.
1227
+ // Patchwork: `patchwork traces export [--output <path>]` — bundle the four
1228
+ // local trace logs into a single .jsonl.gz so a user can move machines,
1229
+ // take a compliance snapshot, or share traces with another tool. See
1230
+ // docs/strategic/2026-05-02/memory-ecosystem-report.md items 1, 3, 12 for
1231
+ // the durability rationale this PR addresses.
1232
+ if (process.argv[2] === "traces" && process.argv[3] === "export") {
1233
+ (async () => {
1234
+ try {
1235
+ const args = process.argv.slice(4);
1236
+ let output;
1237
+ let patchworkDir;
1238
+ let activityDir;
1239
+ for (let i = 0; i < args.length; i++) {
1240
+ const a = args[i];
1241
+ if (a === "--output" || a === "-o") {
1242
+ output = args[i + 1];
1243
+ i++;
1244
+ }
1245
+ else if (a === "--patchwork-dir") {
1246
+ patchworkDir = args[i + 1];
1247
+ i++;
1248
+ }
1249
+ else if (a === "--activity-dir") {
1250
+ activityDir = args[i + 1];
1251
+ i++;
1252
+ }
1253
+ else if (a === "--help" || a === "-h") {
1254
+ process.stdout.write("patchwork traces export [--output <path>] [--patchwork-dir <dir>] [--activity-dir <dir>]\n\n" +
1255
+ "Bundles ~/.patchwork/{runs,decision_traces,commit_issue_links}.jsonl\n" +
1256
+ "and ~/.claude/ide/activity-*.jsonl into a single gzipped JSONL file.\n\n" +
1257
+ "Output is a manifest line followed by one envelope per row:\n" +
1258
+ ' {"type":"manifest", ...}\n' +
1259
+ ' {"source":"runs", "entry":{...}}\n' +
1260
+ " ...\n\n" +
1261
+ "Filter one source with:\n" +
1262
+ " gunzip -c traces-export-*.jsonl.gz | jq 'select(.source==\"decision_traces\") | .entry'\n");
1263
+ process.exit(0);
1264
+ }
1265
+ }
1266
+ const { runTracesExport } = await import("./commands/tracesExport.js");
1267
+ const result = await runTracesExport({
1268
+ ...(output !== undefined && { output }),
1269
+ ...(patchworkDir !== undefined && { patchworkDir }),
1270
+ ...(activityDir !== undefined && { activityDir }),
1271
+ });
1272
+ process.stdout.write(` ✓ Wrote ${result.outputPath}\n`);
1273
+ process.stdout.write(` ${result.totalCount} rows from ${result.files.length} file${result.files.length === 1 ? "" : "s"} (${result.totalBytes} bytes read)\n`);
1274
+ for (const f of result.files) {
1275
+ process.stdout.write(` - ${f.source}: ${f.count} rows (${f.path})\n`);
1276
+ }
1277
+ process.exit(0);
1278
+ }
1279
+ catch (err) {
1280
+ process.stderr.write(`Error: ${err instanceof Error ? err.message : String(err)}\n`);
1281
+ process.exit(1);
1282
+ }
1283
+ })();
1284
+ }
1285
+ // Patchwork: `patchwork traces import <bundle>` — restore an export bundle
1286
+ // into the local patchwork dirs. Closes the half-shipped backup loop.
1287
+ if (process.argv[2] === "traces" && process.argv[3] === "import") {
1288
+ (async () => {
1289
+ try {
1290
+ const args = process.argv.slice(4);
1291
+ let input;
1292
+ let patchworkDir;
1293
+ let activityDir;
1294
+ let mode = "append";
1295
+ let dryRun = false;
1296
+ let passphrase;
1297
+ for (let i = 0; i < args.length; i++) {
1298
+ const a = args[i];
1299
+ if (a === "--patchwork-dir") {
1300
+ patchworkDir = args[i + 1];
1301
+ i++;
1302
+ }
1303
+ else if (a === "--activity-dir") {
1304
+ activityDir = args[i + 1];
1305
+ i++;
1306
+ }
1307
+ else if (a === "--mode") {
1308
+ const m = args[i + 1];
1309
+ if (m !== "append" && m !== "overwrite") {
1310
+ process.stderr.write(`Error: --mode must be "append" or "overwrite" (got: ${m})\n`);
1311
+ process.exit(1);
1312
+ }
1313
+ mode = m;
1314
+ i++;
1315
+ }
1316
+ else if (a === "--passphrase") {
1317
+ passphrase = args[i + 1];
1318
+ i++;
1319
+ }
1320
+ else if (a === "--dry-run") {
1321
+ dryRun = true;
1322
+ }
1323
+ else if (a === "--help" || a === "-h") {
1324
+ process.stdout.write("patchwork traces import <bundle> [--mode append|overwrite] [--dry-run]\n" +
1325
+ " [--passphrase <phrase>]\n" +
1326
+ " [--patchwork-dir <dir>] [--activity-dir <dir>]\n\n" +
1327
+ "Restore a bundle written by `patchwork traces export` into the local\n" +
1328
+ "patchwork dirs (~/.patchwork/ and ~/.claude/ide/ by default).\n\n" +
1329
+ "Formats:\n" +
1330
+ " .jsonl.gz Plain gzip bundle — no passphrase required.\n" +
1331
+ " .enc AES-256-GCM encrypted bundle — pass --passphrase.\n\n" +
1332
+ "Modes:\n" +
1333
+ " append (default) Append rows to existing files.\n" +
1334
+ " overwrite Truncate target files before writing. Use for fresh-machine\n" +
1335
+ " restore; never use when there's local data you want to keep.\n");
1336
+ process.exit(0);
1337
+ }
1338
+ else if (a !== undefined &&
1339
+ !a.startsWith("--") &&
1340
+ input === undefined) {
1341
+ input = a;
1342
+ }
1343
+ }
1344
+ if (!input) {
1345
+ process.stderr.write("Usage: patchwork traces import <bundle> [--passphrase <phrase>] [--mode append|overwrite] [--dry-run]\n");
1346
+ process.exit(1);
1347
+ }
1348
+ // Auto-detect encrypted bundle and decrypt before import.
1349
+ if (passphrase !== undefined || input.endsWith(".enc")) {
1350
+ const { readFileSync } = await import("node:fs");
1351
+ const { isEncryptedTraceBundle, decryptTraceBundle } = await import("./traceEncryption.js");
1352
+ const raw = readFileSync(input);
1353
+ if (isEncryptedTraceBundle(raw)) {
1354
+ if (!passphrase) {
1355
+ process.stderr.write("Error: bundle is encrypted — provide --passphrase <phrase>\n");
1356
+ process.exit(1);
1357
+ }
1358
+ const plain = decryptTraceBundle(raw, passphrase);
1359
+ const { tmpdir } = await import("node:os");
1360
+ const { writeFileSync } = await import("node:fs");
1361
+ const tmp = `${tmpdir()}/patchwork-import-${Date.now()}.jsonl.gz`;
1362
+ writeFileSync(tmp, plain, { mode: 0o600 });
1363
+ input = tmp;
1364
+ process.stderr.write("Decryption succeeded.\n");
1365
+ }
1366
+ }
1367
+ const { runTracesImport } = await import("./commands/tracesImport.js");
1368
+ const result = await runTracesImport({
1369
+ input,
1370
+ ...(patchworkDir !== undefined && { patchworkDir }),
1371
+ ...(activityDir !== undefined && { activityDir }),
1372
+ mode,
1373
+ dryRun,
1374
+ });
1375
+ const verb = result.dryRun
1376
+ ? "Would restore"
1377
+ : result.mode === "overwrite"
1378
+ ? "Restored (overwrite)"
1379
+ : "Restored (append)";
1380
+ process.stdout.write(` ${result.dryRun ? "•" : "✓"} ${verb} ${result.totalCount} rows from ${result.inputPath}\n`);
1381
+ process.stdout.write(` Bundle exportedAt: ${result.exportedAt}\n`);
1382
+ for (const f of result.files) {
1383
+ process.stdout.write(` - ${f.source}: ${f.count} rows → ${f.targetPath}\n`);
1384
+ }
1385
+ process.exit(0);
1386
+ }
1387
+ catch (err) {
1388
+ process.stderr.write(`Error: ${err instanceof Error ? err.message : String(err)}\n`);
1389
+ process.exit(1);
1390
+ }
1391
+ })();
1392
+ }
816
1393
  if (process.argv[2] === "recipe" && process.argv[3] === "schema") {
817
1394
  const outputDir = process.argv[4] ?? path.join(process.cwd(), "schemas");
818
1395
  (async () => {
@@ -836,7 +1413,10 @@ if (process.argv[2] === "recipe" && process.argv[3] === "new") {
836
1413
  const args = process.argv.slice(4);
837
1414
  const recipeName = args[0];
838
1415
  if (!recipeName) {
839
- process.stderr.write("Usage: patchwork recipe new <name> [--template <name>] [--desc <description>]\n");
1416
+ process.stderr.write("Usage: patchwork recipe new <name> [--template <name>] [--desc <description>] [--out <dir>]\n" +
1417
+ " --out <dir> Write the recipe to <dir>/<name>.yaml.\n" +
1418
+ " Defaults to ~/.patchwork/recipes/ — pass `--out .` to\n" +
1419
+ " write into the current directory instead.\n");
840
1420
  process.stderr.write("\nTemplates:\n");
841
1421
  (async () => {
842
1422
  const { listTemplates } = await import("./commands/recipe.js");
@@ -855,10 +1435,16 @@ if (process.argv[2] === "recipe" && process.argv[3] === "new") {
855
1435
  const descIdx = args.indexOf("--desc");
856
1436
  const description = (descIdx >= 0 ? args[descIdx + 1] : undefined) ??
857
1437
  `Recipe: ${recipeName}`;
1438
+ const outIdx = args.indexOf("--out");
1439
+ const outRaw = outIdx >= 0 ? args[outIdx + 1] : undefined;
1440
+ // `--out .` is the common case for "scaffold in cwd" — resolve so
1441
+ // the success message shows the absolute path the user can open.
1442
+ const outputDir = outRaw ? path.resolve(outRaw) : undefined;
858
1443
  const result = runNew({
859
1444
  name: recipeName,
860
1445
  description,
861
1446
  ...(template ? { template } : {}),
1447
+ ...(outputDir ? { outputDir } : {}),
862
1448
  });
863
1449
  process.stdout.write(` ✓ Created ${result.path}\n`);
864
1450
  process.exit(0);
@@ -1962,27 +2548,12 @@ if (process.argv[2] === "launchd") {
1962
2548
  }
1963
2549
  }
1964
2550
  {
1965
- const KNOWN_COMMANDS = [
1966
- "init",
1967
- "patchwork-init",
1968
- "start-all",
1969
- "install-extension",
1970
- "gen-claude-md",
1971
- "print-token",
1972
- "gen-plugin-stub",
1973
- "notify",
1974
- "install",
1975
- "marketplace",
1976
- "status",
1977
- "shim",
1978
- "recipe",
1979
- "dashboard",
1980
- "launchd",
1981
- ];
2551
+ // Reuses the KNOWN_SUBCOMMANDS list from the top of this file as a single
2552
+ // source of truth for "what subcommand argv tokens are recognized".
1982
2553
  const unknownSub = process.argv[2];
1983
2554
  if (unknownSub &&
1984
2555
  !unknownSub.startsWith("-") &&
1985
- !KNOWN_COMMANDS.includes(unknownSub)) {
2556
+ !KNOWN_SUBCOMMANDS.includes(unknownSub)) {
1986
2557
  const lev = (a, b) => {
1987
2558
  const dp = Array.from({ length: a.length + 1 }, (_, i) => Array.from({ length: b.length + 1 }, (_, j) => i === 0 ? j : j === 0 ? i : 0));
1988
2559
  for (let i = 1; i <= a.length; i++)
@@ -1998,127 +2569,146 @@ if (process.argv[2] === "launchd") {
1998
2569
  // biome-ignore lint/style/noNonNullAssertion: dp is fully pre-allocated
1999
2570
  return dp[a.length][b.length];
2000
2571
  };
2001
- const closest = [...KNOWN_COMMANDS].sort((a, b) => lev(unknownSub, a) - lev(unknownSub, b))[0];
2572
+ const closest = [...KNOWN_SUBCOMMANDS].sort((a, b) => lev(unknownSub, a) - lev(unknownSub, b))[0];
2002
2573
  console.error(`Unknown command: '${unknownSub}'. Did you mean: ${closest}?`);
2003
2574
  process.exit(1);
2004
2575
  }
2005
2576
  }
2006
- const config = parseConfig(process.argv);
2007
- // Patchwork: resolve --model flag (optional, non-invasive) stashes the
2008
- // configured adapter on globalThis for consumers that opt into the adapter
2009
- // layer. Bridge subprocess driver still works when --model is absent.
2010
- try {
2011
- const { resolveModel } = await import("./patchworkCli.js");
2012
- const resolved = resolveModel(process.argv);
2013
- if (resolved) {
2014
- globalThis.__patchworkAdapter =
2015
- resolved.adapter;
2016
- process.stderr.write(`[patchwork] model adapter initialized: ${resolved.adapter.name}\n`);
2017
- }
2018
- }
2019
- catch (err) {
2020
- process.stderr.write(`[patchwork] adapter init failed: ${err instanceof Error ? err.message : String(err)}\n`);
2577
+ // Skip the bridge-mode tail entirely when a subcommand IIFE will own the
2578
+ // process. `parseConfig` validates argv against the bridge's known-flag list
2579
+ // and raises "Unknown option" for subcommand-specific flags (e.g. `recipe
2580
+ // new --out .`); without this guard that throw kills the process before
2581
+ // the IIFE's microtask runs. The subcommand handles its own arg parsing.
2582
+ if (__subcommandWillRun) {
2583
+ // Subcommand IIFE is in flight or about to fire; sit tight until it
2584
+ // process.exits. Empty body — control naturally falls past end-of-file
2585
+ // and Node keeps the process alive on the IIFE's pending microtask.
2021
2586
  }
2022
- // If --analytics flag was passed, persist the preference immediately
2023
- if (config.analyticsEnabled !== null) {
2024
- setAnalyticsPref(config.analyticsEnabled);
2025
- }
2026
- // Auto-tmux: if requested and not already inside tmux or screen, re-exec inside a tmux session
2027
- if (config.autoTmux &&
2028
- !process.env.TMUX &&
2029
- !process.env.STY &&
2030
- !process.env.ZELLIJ &&
2031
- !process.env.ZELLIJ_SESSION_NAME) {
2032
- const ws = config.workspace.replace(/[^a-zA-Z0-9]/g, "").slice(-8);
2033
- const hash = crypto
2034
- .createHash("sha256")
2035
- .update(config.workspace)
2036
- .digest("hex")
2037
- .slice(0, 6);
2038
- const sessionName = `claude-bridge-${ws}${hash}`;
2039
- // Check if tmux is available
2040
- const tmuxCheck = spawnSync("which", ["tmux"], { stdio: "ignore" });
2041
- if (tmuxCheck.status !== 0) {
2042
- process.stderr.write("WARNING: --auto-tmux requested but tmux is not installed. Running without tmux.\n");
2043
- }
2044
- else {
2045
- // Strip --auto-tmux from argv to avoid infinite re-exec loop
2046
- const newArgv = process.argv.filter((a) => a !== "--auto-tmux");
2047
- // Pass each argv token as a separate tmux argument so paths with spaces work correctly
2048
- const result = spawnSync("tmux", ["new-session", "-d", "-s", sessionName, ...newArgv], { stdio: "inherit", timeout: 5000 });
2049
- if (result.status === 0) {
2050
- process.stderr.write(`Bridge launched in tmux session '${sessionName}'.\n`);
2051
- process.stderr.write(` Attach with: tmux attach -t ${sessionName}\n`);
2052
- process.exit(0);
2053
- }
2054
- else {
2055
- // tmux session likely already exists — attach to it or fall through
2056
- process.stderr.write(`WARNING: Could not create tmux session '${sessionName}' (already exists?). Running without auto-tmux.\n`);
2587
+ else {
2588
+ const config = parseConfig(process.argv);
2589
+ // Patchwork: resolve --model flag (optional, non-invasive) — stashes the
2590
+ // configured adapter on globalThis for consumers that opt into the adapter
2591
+ // layer. Bridge subprocess driver still works when --model is absent.
2592
+ try {
2593
+ const { resolveModel } = await import("./patchworkCli.js");
2594
+ const resolved = resolveModel(process.argv);
2595
+ if (resolved) {
2596
+ globalThis.__patchworkAdapter =
2597
+ resolved.adapter;
2598
+ process.stderr.write(`[patchwork] model adapter initialized: ${resolved.adapter.name}\n`);
2057
2599
  }
2058
2600
  }
2059
- }
2060
- // --watch: supervisor mode spawn this binary as a child (without --watch) and restart on crash
2061
- if (config.watch) {
2062
- const childArgv = process.argv.filter((a) => a !== "--watch");
2063
- const STABLE_THRESHOLD_MS = 60_000;
2064
- const BASE_DELAY_MS = 2_000;
2065
- const MAX_DELAY_MS = 30_000;
2066
- let delay = BASE_DELAY_MS;
2067
- let stopping = false;
2068
- function runChild() {
2069
- if (stopping)
2070
- return;
2071
- const startAt = Date.now();
2072
- process.stderr.write("[supervisor] starting bridge\n");
2073
- const [cmd, ...args] = childArgv;
2074
- if (!cmd)
2075
- return;
2076
- const child = spawn(cmd, args, {
2077
- stdio: "inherit",
2078
- });
2079
- for (const sig of ["SIGTERM", "SIGINT"]) {
2080
- process.once(sig, () => {
2081
- stopping = true;
2082
- child.kill(sig);
2083
- });
2601
+ catch (err) {
2602
+ process.stderr.write(`[patchwork] adapter init failed: ${err instanceof Error ? err.message : String(err)}\n`);
2603
+ }
2604
+ // If --analytics flag was passed, persist the preference immediately
2605
+ if (config.analyticsEnabled !== null) {
2606
+ setAnalyticsPref(config.analyticsEnabled);
2607
+ }
2608
+ // Auto-tmux: if requested and not already inside tmux or screen, re-exec inside a tmux session
2609
+ if (config.autoTmux &&
2610
+ !process.env.TMUX &&
2611
+ !process.env.STY &&
2612
+ !process.env.ZELLIJ &&
2613
+ !process.env.ZELLIJ_SESSION_NAME) {
2614
+ const ws = config.workspace.replace(/[^a-zA-Z0-9]/g, "").slice(-8);
2615
+ const hash = crypto
2616
+ .createHash("sha256")
2617
+ .update(config.workspace)
2618
+ .digest("hex")
2619
+ .slice(0, 6);
2620
+ const sessionName = `claude-bridge-${ws}${hash}`;
2621
+ // Check if tmux is available
2622
+ const tmuxCheck = spawnSync("which", ["tmux"], { stdio: "ignore" });
2623
+ if (tmuxCheck.status !== 0) {
2624
+ process.stderr.write("WARNING: --auto-tmux requested but tmux is not installed. Running without tmux.\n");
2084
2625
  }
2085
- child.on("exit", (code, signal) => {
2086
- if (stopping) {
2087
- process.stderr.write("[supervisor] bridge stopped\n");
2626
+ else {
2627
+ // Strip --auto-tmux from argv to avoid infinite re-exec loop
2628
+ const newArgv = process.argv.filter((a) => a !== "--auto-tmux");
2629
+ // Pass each argv token as a separate tmux argument so paths with spaces work correctly
2630
+ const result = spawnSync("tmux", ["new-session", "-d", "-s", sessionName, ...newArgv], { stdio: "inherit", timeout: 5000 });
2631
+ if (result.status === 0) {
2632
+ process.stderr.write(`Bridge launched in tmux session '${sessionName}'.\n`);
2633
+ process.stderr.write(` Attach with: tmux attach -t ${sessionName}\n`);
2088
2634
  process.exit(0);
2089
2635
  }
2090
- const uptime = Date.now() - startAt;
2091
- if (uptime >= STABLE_THRESHOLD_MS) {
2092
- delay = BASE_DELAY_MS; // reset backoff after a stable run
2636
+ else {
2637
+ // tmux session likely already exists — attach to it or fall through
2638
+ process.stderr.write(`WARNING: Could not create tmux session '${sessionName}' (already exists?). Running without auto-tmux.\n`);
2093
2639
  }
2094
- process.stderr.write(`[supervisor] bridge exited (code=${code ?? signal}), restarting in ${delay / 1000}s\n`);
2095
- setTimeout(() => {
2096
- delay = Math.min(delay * 2, MAX_DELAY_MS);
2097
- runChild();
2098
- }, delay);
2099
- });
2640
+ }
2100
2641
  }
2101
- runChild();
2102
- }
2103
- else {
2104
- const bridge = new Bridge(config);
2105
- bridge.start().catch((err) => {
2106
- const message = err instanceof Error ? err.message : String(err);
2107
- process.stderr.write(`Error: ${message}\n`);
2108
- process.exit(1);
2109
- });
2110
- // F5: Silent self-update nudge (fire-and-forget)
2111
- import("node:child_process")
2112
- .then(({ exec }) => {
2113
- exec("npm view claude-ide-bridge version", { timeout: 5000 }, (err, stdout) => {
2114
- if (err || !stdout)
2642
+ // Skip bridge boot when a subcommand IIFE is doing the work — avoids the
2643
+ // race where bridge.start() began initialising in parallel with the
2644
+ // subcommand's async path. See the KNOWN_SUBCOMMANDS / __subcommandWillRun
2645
+ // gate at the top of this file.
2646
+ if (__subcommandWillRun) {
2647
+ // intentionally empty subcommand IIFE owns the process from here.
2648
+ }
2649
+ // --watch: supervisor mode — spawn this binary as a child (without --watch) and restart on crash
2650
+ else if (config.watch) {
2651
+ const childArgv = process.argv.filter((a) => a !== "--watch");
2652
+ const STABLE_THRESHOLD_MS = 60_000;
2653
+ const BASE_DELAY_MS = 2_000;
2654
+ const MAX_DELAY_MS = 30_000;
2655
+ let delay = BASE_DELAY_MS;
2656
+ let stopping = false;
2657
+ function runChild() {
2658
+ if (stopping)
2115
2659
  return;
2116
- const latest = stdout.trim();
2117
- if (latest && semverGt(latest, PACKAGE_VERSION)) {
2118
- console.log(`\n Bridge v${latest} available run: npm update -g claude-ide-bridge\n`);
2660
+ const startAt = Date.now();
2661
+ process.stderr.write("[supervisor] starting bridge\n");
2662
+ const [cmd, ...args] = childArgv;
2663
+ if (!cmd)
2664
+ return;
2665
+ const child = spawn(cmd, args, {
2666
+ stdio: "inherit",
2667
+ });
2668
+ for (const sig of ["SIGTERM", "SIGINT"]) {
2669
+ process.once(sig, () => {
2670
+ stopping = true;
2671
+ child.kill(sig);
2672
+ });
2119
2673
  }
2674
+ child.on("exit", (code, signal) => {
2675
+ if (stopping) {
2676
+ process.stderr.write("[supervisor] bridge stopped\n");
2677
+ process.exit(0);
2678
+ }
2679
+ const uptime = Date.now() - startAt;
2680
+ if (uptime >= STABLE_THRESHOLD_MS) {
2681
+ delay = BASE_DELAY_MS; // reset backoff after a stable run
2682
+ }
2683
+ process.stderr.write(`[supervisor] bridge exited (code=${code ?? signal}), restarting in ${delay / 1000}s\n`);
2684
+ setTimeout(() => {
2685
+ delay = Math.min(delay * 2, MAX_DELAY_MS);
2686
+ runChild();
2687
+ }, delay);
2688
+ });
2689
+ }
2690
+ runChild();
2691
+ }
2692
+ else {
2693
+ const bridge = new Bridge(config);
2694
+ bridge.start().catch((err) => {
2695
+ const message = err instanceof Error ? err.message : String(err);
2696
+ process.stderr.write(`Error: ${message}\n`);
2697
+ process.exit(1);
2120
2698
  });
2121
- })
2122
- .catch(() => { });
2123
- }
2699
+ // F5: Silent self-update nudge (fire-and-forget)
2700
+ import("node:child_process")
2701
+ .then(({ exec }) => {
2702
+ exec("npm view claude-ide-bridge version", { timeout: 5000 }, (err, stdout) => {
2703
+ if (err || !stdout)
2704
+ return;
2705
+ const latest = stdout.trim();
2706
+ if (latest && semverGt(latest, PACKAGE_VERSION)) {
2707
+ console.log(`\n Bridge v${latest} available — run: npm update -g claude-ide-bridge\n`);
2708
+ }
2709
+ });
2710
+ })
2711
+ .catch(() => { });
2712
+ }
2713
+ } // end of `else` for `if (__subcommandWillRun)` (bridge-mode block)
2124
2714
  //# sourceMappingURL=index.js.map