patchwork-os 0.2.0-alpha.2 → 0.2.0-alpha.21

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 (154) hide show
  1. package/README.bridge.md +6 -0
  2. package/README.md +13 -2
  3. package/dist/approvalHttp.d.ts +11 -2
  4. package/dist/approvalHttp.js +92 -9
  5. package/dist/approvalHttp.js.map +1 -1
  6. package/dist/approvalQueue.d.ts +12 -1
  7. package/dist/approvalQueue.js +25 -3
  8. package/dist/approvalQueue.js.map +1 -1
  9. package/dist/bridge.js +127 -23
  10. package/dist/bridge.js.map +1 -1
  11. package/dist/claudeDriver.d.ts +3 -1
  12. package/dist/claudeDriver.js +48 -0
  13. package/dist/claudeDriver.js.map +1 -1
  14. package/dist/claudeOrchestrator.d.ts +1 -1
  15. package/dist/claudeOrchestrator.js +14 -8
  16. package/dist/claudeOrchestrator.js.map +1 -1
  17. package/dist/commands/launchd.d.ts +2 -0
  18. package/dist/commands/launchd.js +94 -0
  19. package/dist/commands/launchd.js.map +1 -0
  20. package/dist/config.d.ts +7 -2
  21. package/dist/config.js +85 -8
  22. package/dist/config.js.map +1 -1
  23. package/dist/connectors/github.d.ts +58 -8
  24. package/dist/connectors/github.js +321 -84
  25. package/dist/connectors/github.js.map +1 -1
  26. package/dist/connectors/gmail.d.ts +4 -1
  27. package/dist/connectors/gmail.js +77 -16
  28. package/dist/connectors/gmail.js.map +1 -1
  29. package/dist/connectors/googleCalendar.d.ts +60 -0
  30. package/dist/connectors/googleCalendar.js +329 -0
  31. package/dist/connectors/googleCalendar.js.map +1 -0
  32. package/dist/connectors/linear.d.ts +117 -0
  33. package/dist/connectors/linear.js +248 -0
  34. package/dist/connectors/linear.js.map +1 -0
  35. package/dist/connectors/mcpClient.d.ts +56 -0
  36. package/dist/connectors/mcpClient.js +189 -0
  37. package/dist/connectors/mcpClient.js.map +1 -0
  38. package/dist/connectors/mcpOAuth.d.ts +83 -0
  39. package/dist/connectors/mcpOAuth.js +363 -0
  40. package/dist/connectors/mcpOAuth.js.map +1 -0
  41. package/dist/connectors/sentry.d.ts +43 -0
  42. package/dist/connectors/sentry.js +197 -0
  43. package/dist/connectors/sentry.js.map +1 -0
  44. package/dist/connectors/slack.d.ts +50 -0
  45. package/dist/connectors/slack.js +289 -0
  46. package/dist/connectors/slack.js.map +1 -0
  47. package/dist/drivers/claude/api.d.ts +11 -0
  48. package/dist/drivers/claude/api.js +54 -0
  49. package/dist/drivers/claude/api.js.map +1 -0
  50. package/dist/drivers/claude/envSanitizer.d.ts +7 -0
  51. package/dist/drivers/claude/envSanitizer.js +18 -0
  52. package/dist/drivers/claude/envSanitizer.js.map +1 -0
  53. package/dist/drivers/claude/streamParser.d.ts +38 -0
  54. package/dist/drivers/claude/streamParser.js +34 -0
  55. package/dist/drivers/claude/streamParser.js.map +1 -0
  56. package/dist/drivers/claude/subprocess.d.ts +19 -0
  57. package/dist/drivers/claude/subprocess.js +216 -0
  58. package/dist/drivers/claude/subprocess.js.map +1 -0
  59. package/dist/drivers/claude/subprocessSettings.d.ts +9 -0
  60. package/dist/drivers/claude/subprocessSettings.js +55 -0
  61. package/dist/drivers/claude/subprocessSettings.js.map +1 -0
  62. package/dist/drivers/gemini/index.d.ts +18 -0
  63. package/dist/drivers/gemini/index.js +210 -0
  64. package/dist/drivers/gemini/index.js.map +1 -0
  65. package/dist/drivers/grok/index.d.ts +11 -0
  66. package/dist/drivers/grok/index.js +22 -0
  67. package/dist/drivers/grok/index.js.map +1 -0
  68. package/dist/drivers/index.d.ts +23 -0
  69. package/dist/drivers/index.js +31 -0
  70. package/dist/drivers/index.js.map +1 -0
  71. package/dist/drivers/openai/index.d.ts +24 -0
  72. package/dist/drivers/openai/index.js +110 -0
  73. package/dist/drivers/openai/index.js.map +1 -0
  74. package/dist/drivers/types.d.ts +72 -0
  75. package/dist/drivers/types.js +30 -0
  76. package/dist/drivers/types.js.map +1 -0
  77. package/dist/index.js +35 -1
  78. package/dist/index.js.map +1 -1
  79. package/dist/installGuard.d.ts +25 -0
  80. package/dist/installGuard.js +48 -0
  81. package/dist/installGuard.js.map +1 -0
  82. package/dist/patchworkConfig.d.ts +9 -0
  83. package/dist/patchworkConfig.js.map +1 -1
  84. package/dist/recipes/scheduler.d.ts +23 -7
  85. package/dist/recipes/scheduler.js +135 -41
  86. package/dist/recipes/scheduler.js.map +1 -1
  87. package/dist/recipes/yamlRunner.d.ts +15 -0
  88. package/dist/recipes/yamlRunner.js +325 -26
  89. package/dist/recipes/yamlRunner.js.map +1 -1
  90. package/dist/recipesHttp.d.ts +14 -1
  91. package/dist/recipesHttp.js +21 -4
  92. package/dist/recipesHttp.js.map +1 -1
  93. package/dist/runLog.d.ts +5 -0
  94. package/dist/runLog.js +51 -1
  95. package/dist/runLog.js.map +1 -1
  96. package/dist/server.d.ts +15 -1
  97. package/dist/server.js +458 -31
  98. package/dist/server.js.map +1 -1
  99. package/dist/tools/addLinearComment.d.ts +55 -0
  100. package/dist/tools/addLinearComment.js +72 -0
  101. package/dist/tools/addLinearComment.js.map +1 -0
  102. package/dist/tools/bridgeDoctor.js +2 -2
  103. package/dist/tools/bridgeDoctor.js.map +1 -1
  104. package/dist/tools/createLinearIssue.d.ts +84 -0
  105. package/dist/tools/createLinearIssue.js +146 -0
  106. package/dist/tools/createLinearIssue.js.map +1 -0
  107. package/dist/tools/ctxGetTaskContext.d.ts +4 -1
  108. package/dist/tools/ctxGetTaskContext.js +45 -2
  109. package/dist/tools/ctxGetTaskContext.js.map +1 -1
  110. package/dist/tools/fetchCalendarEvents.d.ts +94 -0
  111. package/dist/tools/fetchCalendarEvents.js +97 -0
  112. package/dist/tools/fetchCalendarEvents.js.map +1 -0
  113. package/dist/tools/fetchGithubIssue.d.ts +80 -0
  114. package/dist/tools/fetchGithubIssue.js +84 -0
  115. package/dist/tools/fetchGithubIssue.js.map +1 -0
  116. package/dist/tools/fetchGithubPR.d.ts +89 -0
  117. package/dist/tools/fetchGithubPR.js +96 -0
  118. package/dist/tools/fetchGithubPR.js.map +1 -0
  119. package/dist/tools/fetchLinearIssue.d.ts +112 -0
  120. package/dist/tools/fetchLinearIssue.js +129 -0
  121. package/dist/tools/fetchLinearIssue.js.map +1 -0
  122. package/dist/tools/fetchSentryIssue.d.ts +143 -0
  123. package/dist/tools/fetchSentryIssue.js +150 -0
  124. package/dist/tools/fetchSentryIssue.js.map +1 -0
  125. package/dist/tools/fetchSlackProfile.d.ts +43 -0
  126. package/dist/tools/fetchSlackProfile.js +46 -0
  127. package/dist/tools/fetchSlackProfile.js.map +1 -0
  128. package/dist/tools/getConnectorStatus.d.ts +58 -0
  129. package/dist/tools/getConnectorStatus.js +56 -0
  130. package/dist/tools/getConnectorStatus.js.map +1 -0
  131. package/dist/tools/github/index.d.ts +1 -1
  132. package/dist/tools/github/index.js +1 -1
  133. package/dist/tools/github/index.js.map +1 -1
  134. package/dist/tools/github/pr.d.ts +122 -0
  135. package/dist/tools/github/pr.js +183 -0
  136. package/dist/tools/github/pr.js.map +1 -1
  137. package/dist/tools/index.js +27 -1
  138. package/dist/tools/index.js.map +1 -1
  139. package/dist/tools/slackListChannels.d.ts +65 -0
  140. package/dist/tools/slackListChannels.js +70 -0
  141. package/dist/tools/slackListChannels.js.map +1 -0
  142. package/dist/tools/slackPostMessage.d.ts +57 -0
  143. package/dist/tools/slackPostMessage.js +77 -0
  144. package/dist/tools/slackPostMessage.js.map +1 -0
  145. package/dist/tools/updateLinearIssue.d.ts +89 -0
  146. package/dist/tools/updateLinearIssue.js +117 -0
  147. package/dist/tools/updateLinearIssue.js.map +1 -0
  148. package/package.json +4 -2
  149. package/scripts/start-all.sh +56 -19
  150. package/templates/co.patchwork-os.bridge.plist +34 -0
  151. package/templates/recipes/ctx-loop-test.yaml +75 -0
  152. package/templates/recipes/morning-brief-slack.yaml +57 -0
  153. package/templates/recipes/morning-brief.yaml +21 -5
  154. package/templates/recipes/sentry-to-linear.yaml +77 -0
package/dist/server.js CHANGED
@@ -5,6 +5,7 @@ import path from "node:path";
5
5
  import { WebSocket, WebSocketServer as WsServer } from "ws";
6
6
  import { routeApprovalRequest } from "./approvalHttp.js";
7
7
  import { getApprovalQueue } from "./approvalQueue.js";
8
+ import { saveBridgeConfigDriver } from "./config.js";
8
9
  import { timingSafeStringEqual } from "./crypto.js";
9
10
  import { renderDashboardHtml } from "./dashboard.js";
10
11
  import { loadConfig as loadPatchworkConfig, defaultConfigPath as patchworkConfigPath, saveConfig as savePatchworkConfig, } from "./patchworkConfig.js";
@@ -97,10 +98,18 @@ export class Server extends EventEmitter {
97
98
  runRecipeFn = null;
98
99
  /** Patchwork: admin-controlled managed settings path (highest rule precedence). */
99
100
  managedSettingsPath = undefined;
101
+ /** Effective bridge config path to update when dashboard saves driver changes. */
102
+ bridgeConfigPath = undefined;
100
103
  /** Patchwork: live approval gate level — mutated by POST /settings, read by bridge per-session setup. */
101
104
  approvalGate = "off";
102
105
  /** Patchwork: outbound webhook URL for approval notifications (from dashboard.webhookUrl in config). */
103
106
  approvalWebhookUrl = undefined;
107
+ /** Patchwork: push relay service URL — when set, per-callId approval tokens are generated. */
108
+ pushServiceUrl = undefined;
109
+ /** Patchwork: bearer token for the push relay service. */
110
+ pushServiceToken = undefined;
111
+ /** Patchwork: public base URL of this bridge, embedded in push payloads as callback base. */
112
+ pushServiceBaseUrl = undefined;
104
113
  /** Patchwork: approval decision audit callback wired to activityLog.recordEvent. */
105
114
  onApprovalDecision = undefined;
106
115
  /** Patchwork: set by bridge to match + fire webhook-triggered recipes. */
@@ -127,6 +136,7 @@ export class Server extends EventEmitter {
127
136
  sessionDetailFn = null;
128
137
  /** Set by bridge to handle POST /launch-quick-task — invokes launchQuickTask tool in-process. */
129
138
  launchQuickTaskFn = null;
139
+ setRecipeEnabledFn = null;
130
140
  /**
131
141
  * Attach an OAuth 2.0 Authorization Server.
132
142
  * When set, the bridge exposes:
@@ -355,6 +365,97 @@ export class Server extends EventEmitter {
355
365
  res.end(JSON.stringify({ ok: true, v: PACKAGE_VERSION }));
356
366
  return;
357
367
  }
368
+ // ── Connector OAuth callbacks (unauthenticated — browser redirect from vendor) ──
369
+ if (parsedUrl.pathname === "/connections/github/callback" &&
370
+ req.method === "GET") {
371
+ void (async () => {
372
+ const { handleGithubCallback } = await import("./connectors/github.js");
373
+ const code = parsedUrl.searchParams.get("code");
374
+ const state = parsedUrl.searchParams.get("state");
375
+ const error = parsedUrl.searchParams.get("error");
376
+ const result = await handleGithubCallback(code, state, error);
377
+ res.writeHead(result.status, {
378
+ "Content-Type": result.contentType ?? "application/json",
379
+ });
380
+ res.end(result.body);
381
+ })();
382
+ return;
383
+ }
384
+ if (parsedUrl.pathname === "/connections/linear/callback" &&
385
+ req.method === "GET") {
386
+ void (async () => {
387
+ const { handleLinearCallback } = await import("./connectors/linear.js");
388
+ const code = parsedUrl.searchParams.get("code");
389
+ const state = parsedUrl.searchParams.get("state");
390
+ const error = parsedUrl.searchParams.get("error");
391
+ const result = await handleLinearCallback(code, state, error);
392
+ res.writeHead(result.status, {
393
+ "Content-Type": result.contentType ?? "application/json",
394
+ });
395
+ res.end(result.body);
396
+ })();
397
+ return;
398
+ }
399
+ if (parsedUrl.pathname === "/connections/sentry/callback" &&
400
+ req.method === "GET") {
401
+ void (async () => {
402
+ const { handleSentryCallback } = await import("./connectors/sentry.js");
403
+ const code = parsedUrl.searchParams.get("code");
404
+ const state = parsedUrl.searchParams.get("state");
405
+ const error = parsedUrl.searchParams.get("error");
406
+ const result = await handleSentryCallback(code, state, error);
407
+ res.writeHead(result.status, {
408
+ "Content-Type": result.contentType ?? "application/json",
409
+ });
410
+ res.end(result.body);
411
+ })();
412
+ return;
413
+ }
414
+ if (parsedUrl.pathname === "/connections/google-calendar/callback" &&
415
+ req.method === "GET") {
416
+ void (async () => {
417
+ const { handleCalendarCallback } = await import("./connectors/googleCalendar.js");
418
+ const code = parsedUrl.searchParams.get("code");
419
+ const state = parsedUrl.searchParams.get("state");
420
+ const error = parsedUrl.searchParams.get("error");
421
+ const result = await handleCalendarCallback(code, state, error);
422
+ res.writeHead(result.status, {
423
+ "Content-Type": result.contentType ?? "application/json",
424
+ });
425
+ res.end(result.body);
426
+ })();
427
+ return;
428
+ }
429
+ if (parsedUrl.pathname === "/connections/slack/callback" &&
430
+ req.method === "GET") {
431
+ void (async () => {
432
+ const { handleSlackCallback } = await import("./connectors/slack.js");
433
+ const code = parsedUrl.searchParams.get("code");
434
+ const state = parsedUrl.searchParams.get("state");
435
+ const error = parsedUrl.searchParams.get("error");
436
+ const result = await handleSlackCallback(code, state, error);
437
+ res.writeHead(result.status, {
438
+ "Content-Type": result.contentType ?? "application/json",
439
+ });
440
+ res.end(result.body);
441
+ })();
442
+ return;
443
+ }
444
+ if (parsedUrl.pathname === "/connections/gmail/callback" &&
445
+ req.method === "GET") {
446
+ void (async () => {
447
+ const { handleGmailCallback } = await import("./connectors/gmail.js");
448
+ const code = parsedUrl.searchParams.get("code");
449
+ const state = parsedUrl.searchParams.get("state");
450
+ const error = parsedUrl.searchParams.get("error");
451
+ const result = await handleGmailCallback(code, state, error);
452
+ res.writeHead(result.status, {
453
+ "Content-Type": result.contentType ?? "text/html",
454
+ });
455
+ res.end(result.body);
456
+ })();
457
+ return;
458
+ }
358
459
  // ── Bearer token authentication ───────────────────────────────────────
359
460
  // All other HTTP endpoints require a valid Bearer token.
360
461
  // Accepts either:
@@ -372,8 +473,13 @@ export class Server extends EventEmitter {
372
473
  const oauthResolved = !isStaticToken && this.oauthServer
373
474
  ? this.oauthServer.resolveBearerToken(bearer)
374
475
  : null;
476
+ // Phone-path: approve/reject with x-approval-token bypass bearer check.
477
+ // The token itself is validated inside routeApprovalRequest via queue.validateToken.
478
+ const isPhoneApprovalPath = req.method === "POST" &&
479
+ /^\/(approve|reject)\/[A-Za-z0-9-]+$/.test(parsedUrl.pathname) &&
480
+ !!req.headers["x-approval-token"];
375
481
  // oauthResolved is the bridge token if the OAuth token is valid; null otherwise
376
- if (!isStaticToken && !oauthResolved) {
482
+ if (!isStaticToken && !oauthResolved && !isPhoneApprovalPath) {
377
483
  // RFC 6750: only include error= when a token was actually presented but invalid
378
484
  const tokenPresented = bearer.length > 0;
379
485
  const wwwAuth = this.oauthServer && this.oauthIssuerUrl
@@ -675,26 +781,124 @@ export class Server extends EventEmitter {
675
781
  })();
676
782
  return;
677
783
  }
678
- if (parsedUrl.pathname === "/connections/gmail/callback" &&
784
+ if (parsedUrl.pathname === "/connections/gmail" &&
785
+ req.method === "DELETE") {
786
+ void (async () => {
787
+ const { handleGmailDisconnect } = await import("./connectors/gmail.js");
788
+ const result = await handleGmailDisconnect();
789
+ res.writeHead(result.status, {
790
+ "Content-Type": result.contentType ?? "application/json",
791
+ });
792
+ res.end(result.body);
793
+ })();
794
+ return;
795
+ }
796
+ if (parsedUrl.pathname === "/connections/gmail/test" &&
797
+ req.method === "POST") {
798
+ void (async () => {
799
+ const { handleGmailTest } = await import("./connectors/gmail.js");
800
+ const result = await handleGmailTest();
801
+ res.writeHead(result.status, {
802
+ "Content-Type": result.contentType ?? "application/json",
803
+ });
804
+ res.end(result.body);
805
+ })();
806
+ return;
807
+ }
808
+ // ── GitHub MCP connector routes ─────────────────────────────────────
809
+ if (parsedUrl.pathname === "/connections/github/auth" &&
679
810
  req.method === "GET") {
680
811
  void (async () => {
681
- const { handleGmailCallback } = await import("./connectors/gmail.js");
812
+ const { handleGithubAuthorize } = await import("./connectors/github.js");
813
+ const result = await handleGithubAuthorize();
814
+ if (result.redirect) {
815
+ res.writeHead(302, { Location: result.redirect });
816
+ res.end();
817
+ }
818
+ else {
819
+ res.writeHead(result.status, {
820
+ "Content-Type": result.contentType ?? "application/json",
821
+ });
822
+ res.end(result.body);
823
+ }
824
+ })();
825
+ return;
826
+ }
827
+ if (parsedUrl.pathname === "/connections/github/test" &&
828
+ req.method === "POST") {
829
+ void (async () => {
830
+ const { handleGithubTest } = await import("./connectors/github.js");
831
+ const result = await handleGithubTest();
832
+ res.writeHead(result.status, {
833
+ "Content-Type": result.contentType ?? "application/json",
834
+ });
835
+ res.end(result.body);
836
+ })();
837
+ return;
838
+ }
839
+ if (parsedUrl.pathname === "/connections/github" &&
840
+ req.method === "DELETE") {
841
+ void (async () => {
842
+ const { handleGithubDisconnect } = await import("./connectors/github.js");
843
+ const result = await handleGithubDisconnect();
844
+ res.writeHead(result.status, {
845
+ "Content-Type": result.contentType ?? "application/json",
846
+ });
847
+ res.end(result.body);
848
+ })();
849
+ return;
850
+ }
851
+ // ── Sentry MCP connector routes ─────────────────────────────────────
852
+ if (parsedUrl.pathname === "/connections/sentry/auth" &&
853
+ req.method === "GET") {
854
+ void (async () => {
855
+ const { handleSentryAuthorize } = await import("./connectors/sentry.js");
856
+ const result = await handleSentryAuthorize();
857
+ if (result.redirect) {
858
+ res.writeHead(302, { Location: result.redirect });
859
+ res.end();
860
+ }
861
+ else {
862
+ res.writeHead(result.status, {
863
+ "Content-Type": result.contentType ?? "application/json",
864
+ });
865
+ res.end(result.body);
866
+ }
867
+ })();
868
+ return;
869
+ }
870
+ if (parsedUrl.pathname === "/connections/sentry/callback" &&
871
+ req.method === "GET") {
872
+ void (async () => {
873
+ const { handleSentryCallback } = await import("./connectors/sentry.js");
682
874
  const code = parsedUrl.searchParams.get("code");
683
875
  const state = parsedUrl.searchParams.get("state");
684
876
  const error = parsedUrl.searchParams.get("error");
685
- const result = await handleGmailCallback(code, state, error);
877
+ const result = await handleSentryCallback(code, state, error);
686
878
  res.writeHead(result.status, {
687
- "Content-Type": result.contentType ?? "text/html",
879
+ "Content-Type": result.contentType ?? "application/json",
688
880
  });
689
881
  res.end(result.body);
690
882
  })();
691
883
  return;
692
884
  }
693
- if (parsedUrl.pathname === "/connections/gmail" &&
885
+ if (parsedUrl.pathname === "/connections/sentry/test" &&
886
+ req.method === "POST") {
887
+ void (async () => {
888
+ const { handleSentryTest } = await import("./connectors/sentry.js");
889
+ const result = await handleSentryTest();
890
+ res.writeHead(result.status, {
891
+ "Content-Type": result.contentType ?? "application/json",
892
+ });
893
+ res.end(result.body);
894
+ })();
895
+ return;
896
+ }
897
+ if (parsedUrl.pathname === "/connections/sentry" &&
694
898
  req.method === "DELETE") {
695
899
  void (async () => {
696
- const { handleGmailDisconnect } = await import("./connectors/gmail.js");
697
- const result = await handleGmailDisconnect();
900
+ const { handleSentryDisconnect } = await import("./connectors/sentry.js");
901
+ const result = await handleSentryDisconnect();
698
902
  res.writeHead(result.status, {
699
903
  "Content-Type": result.contentType ?? "application/json",
700
904
  });
@@ -702,11 +906,45 @@ export class Server extends EventEmitter {
702
906
  })();
703
907
  return;
704
908
  }
705
- if (parsedUrl.pathname === "/connections/gmail/test" &&
909
+ // ── Linear MCP connector routes ─────────────────────────────────────
910
+ if (parsedUrl.pathname === "/connections/linear/auth" &&
911
+ req.method === "GET") {
912
+ void (async () => {
913
+ const { handleLinearAuthorize } = await import("./connectors/linear.js");
914
+ const result = await handleLinearAuthorize();
915
+ if (result.redirect) {
916
+ res.writeHead(302, { Location: result.redirect });
917
+ res.end();
918
+ }
919
+ else {
920
+ res.writeHead(result.status, {
921
+ "Content-Type": result.contentType ?? "application/json",
922
+ });
923
+ res.end(result.body);
924
+ }
925
+ })();
926
+ return;
927
+ }
928
+ if (parsedUrl.pathname === "/connections/linear/callback" &&
929
+ req.method === "GET") {
930
+ void (async () => {
931
+ const { handleLinearCallback } = await import("./connectors/linear.js");
932
+ const code = parsedUrl.searchParams.get("code");
933
+ const state = parsedUrl.searchParams.get("state");
934
+ const error = parsedUrl.searchParams.get("error");
935
+ const result = await handleLinearCallback(code, state, error);
936
+ res.writeHead(result.status, {
937
+ "Content-Type": result.contentType ?? "application/json",
938
+ });
939
+ res.end(result.body);
940
+ })();
941
+ return;
942
+ }
943
+ if (parsedUrl.pathname === "/connections/linear/test" &&
706
944
  req.method === "POST") {
707
945
  void (async () => {
708
- const { handleGmailTest } = await import("./connectors/gmail.js");
709
- const result = await handleGmailTest();
946
+ const { handleLinearTest } = await import("./connectors/linear.js");
947
+ const result = await handleLinearTest();
710
948
  res.writeHead(result.status, {
711
949
  "Content-Type": result.contentType ?? "application/json",
712
950
  });
@@ -714,28 +952,116 @@ export class Server extends EventEmitter {
714
952
  })();
715
953
  return;
716
954
  }
717
- if (parsedUrl.pathname === "/connections/github/test" &&
955
+ if (parsedUrl.pathname === "/connections/linear" &&
956
+ req.method === "DELETE") {
957
+ void (async () => {
958
+ const { handleLinearDisconnect } = await import("./connectors/linear.js");
959
+ const result = await handleLinearDisconnect();
960
+ res.writeHead(result.status, {
961
+ "Content-Type": result.contentType ?? "application/json",
962
+ });
963
+ res.end(result.body);
964
+ })();
965
+ return;
966
+ }
967
+ // ── Slack connector routes ──────────────────────────────────────
968
+ if ((parsedUrl.pathname === "/connections/slack/auth" ||
969
+ parsedUrl.pathname === "/connections/slack/authorize") &&
970
+ req.method === "GET") {
971
+ const { handleSlackAuthorize } = await import("./connectors/slack.js");
972
+ const result = handleSlackAuthorize();
973
+ if (result.redirect) {
974
+ res.writeHead(302, { Location: result.redirect });
975
+ res.end();
976
+ }
977
+ else {
978
+ res.writeHead(result.status, {
979
+ "Content-Type": result.contentType ?? "application/json",
980
+ });
981
+ res.end(result.body);
982
+ }
983
+ return;
984
+ }
985
+ if (parsedUrl.pathname === "/connections/slack/test" &&
718
986
  req.method === "POST") {
719
987
  void (async () => {
720
- const { getStatus } = await import("./connectors/github.js");
721
- const s = getStatus();
722
- if (s.connected) {
723
- res.writeHead(200, { "Content-Type": "application/json" });
724
- res.end(JSON.stringify({
725
- ok: true,
726
- message: `Connected as ${s.user ?? "unknown"}`,
727
- }));
988
+ const { handleSlackTest } = await import("./connectors/slack.js");
989
+ const result = await handleSlackTest();
990
+ res.writeHead(result.status, {
991
+ "Content-Type": result.contentType ?? "application/json",
992
+ });
993
+ res.end(result.body);
994
+ })();
995
+ return;
996
+ }
997
+ if (parsedUrl.pathname === "/connections/slack" &&
998
+ req.method === "DELETE") {
999
+ const { handleSlackDisconnect } = await import("./connectors/slack.js");
1000
+ const result = handleSlackDisconnect();
1001
+ res.writeHead(result.status, {
1002
+ "Content-Type": result.contentType ?? "application/json",
1003
+ });
1004
+ res.end(result.body);
1005
+ return;
1006
+ }
1007
+ // ── Google Calendar routes ──────────────────────────────────────
1008
+ if (parsedUrl.pathname === "/connections/google-calendar/auth" &&
1009
+ req.method === "GET") {
1010
+ void (async () => {
1011
+ const { handleCalendarAuthRedirect } = await import("./connectors/googleCalendar.js");
1012
+ const result = handleCalendarAuthRedirect();
1013
+ if (result.redirect) {
1014
+ res.writeHead(302, { Location: result.redirect });
1015
+ res.end();
728
1016
  }
729
1017
  else {
730
- res.writeHead(200, { "Content-Type": "application/json" });
731
- res.end(JSON.stringify({
732
- ok: false,
733
- message: "Not connected — run: gh auth login",
734
- }));
1018
+ res.writeHead(result.status, {
1019
+ "Content-Type": result.contentType ?? "application/json",
1020
+ });
1021
+ res.end(result.body);
735
1022
  }
736
1023
  })();
737
1024
  return;
738
1025
  }
1026
+ if (parsedUrl.pathname === "/connections/google-calendar/callback" &&
1027
+ req.method === "GET") {
1028
+ void (async () => {
1029
+ const { handleCalendarCallback } = await import("./connectors/googleCalendar.js");
1030
+ const code = parsedUrl.searchParams.get("code");
1031
+ const state = parsedUrl.searchParams.get("state");
1032
+ const error = parsedUrl.searchParams.get("error");
1033
+ const result = await handleCalendarCallback(code, state, error);
1034
+ res.writeHead(result.status, {
1035
+ "Content-Type": result.contentType ?? "application/json",
1036
+ });
1037
+ res.end(result.body);
1038
+ })();
1039
+ return;
1040
+ }
1041
+ if (parsedUrl.pathname === "/connections/google-calendar/test" &&
1042
+ req.method === "POST") {
1043
+ void (async () => {
1044
+ const { handleCalendarTest } = await import("./connectors/googleCalendar.js");
1045
+ const result = await handleCalendarTest();
1046
+ res.writeHead(result.status, {
1047
+ "Content-Type": result.contentType ?? "application/json",
1048
+ });
1049
+ res.end(result.body);
1050
+ })();
1051
+ return;
1052
+ }
1053
+ if (parsedUrl.pathname === "/connections/google-calendar" &&
1054
+ req.method === "DELETE") {
1055
+ void (async () => {
1056
+ const { handleCalendarDisconnect } = await import("./connectors/googleCalendar.js");
1057
+ const result = await handleCalendarDisconnect();
1058
+ res.writeHead(result.status, {
1059
+ "Content-Type": result.contentType ?? "application/json",
1060
+ });
1061
+ res.end(result.body);
1062
+ })();
1063
+ return;
1064
+ }
739
1065
  // ── Inbox routes ────────────────────────────────────────────────────
740
1066
  if (parsedUrl.pathname === "/inbox" && req.method === "GET") {
741
1067
  void (async () => {
@@ -831,6 +1157,11 @@ export class Server extends EventEmitter {
831
1157
  const body = Buffer.concat(chunks).toString("utf-8");
832
1158
  const parsed = JSON.parse(body || "{}");
833
1159
  const name = parsed.name;
1160
+ const vars = parsed.vars &&
1161
+ typeof parsed.vars === "object" &&
1162
+ !Array.isArray(parsed.vars)
1163
+ ? parsed.vars
1164
+ : undefined;
834
1165
  if (typeof name !== "string" || !name) {
835
1166
  res.writeHead(400, { "Content-Type": "application/json" });
836
1167
  res.end(JSON.stringify({ ok: false, error: "name required" }));
@@ -844,7 +1175,7 @@ export class Server extends EventEmitter {
844
1175
  }));
845
1176
  return;
846
1177
  }
847
- const result = await this.runRecipeFn(name);
1178
+ const result = await this.runRecipeFn(name, vars);
848
1179
  res.writeHead(result.ok ? 200 : 400, {
849
1180
  "Content-Type": "application/json",
850
1181
  });
@@ -919,6 +1250,40 @@ export class Server extends EventEmitter {
919
1250
  });
920
1251
  return;
921
1252
  }
1253
+ const recipePatchMatch = /^\/recipes\/([^/]+)$/.exec(parsedUrl.pathname);
1254
+ if (recipePatchMatch && req.method === "PATCH") {
1255
+ const name = decodeURIComponent(recipePatchMatch[1]);
1256
+ const chunks = [];
1257
+ req.on("data", (c) => chunks.push(c));
1258
+ req.on("end", () => {
1259
+ try {
1260
+ const body = JSON.parse(Buffer.concat(chunks).toString("utf-8"));
1261
+ if (typeof body.enabled !== "boolean") {
1262
+ res.writeHead(400, { "Content-Type": "application/json" });
1263
+ res.end(JSON.stringify({
1264
+ ok: false,
1265
+ error: "enabled (boolean) required",
1266
+ }));
1267
+ return;
1268
+ }
1269
+ if (!this.setRecipeEnabledFn) {
1270
+ res.writeHead(503, { "Content-Type": "application/json" });
1271
+ res.end(JSON.stringify({ ok: false, error: "Not available" }));
1272
+ return;
1273
+ }
1274
+ const result = this.setRecipeEnabledFn(name, body.enabled);
1275
+ res.writeHead(result.ok ? 200 : 400, {
1276
+ "Content-Type": "application/json",
1277
+ });
1278
+ res.end(JSON.stringify(result));
1279
+ }
1280
+ catch {
1281
+ res.writeHead(400, { "Content-Type": "application/json" });
1282
+ res.end(JSON.stringify({ ok: false, error: "Invalid JSON" }));
1283
+ }
1284
+ });
1285
+ return;
1286
+ }
922
1287
  if (req.url === "/recipes" && req.method === "GET") {
923
1288
  try {
924
1289
  const data = this.recipesFn?.() ?? { recipesDir: null, recipes: [] };
@@ -979,8 +1344,11 @@ export class Server extends EventEmitter {
979
1344
  req.on("end", () => {
980
1345
  try {
981
1346
  const body = JSON.parse(Buffer.concat(chunks).toString("utf-8"));
982
- const raw = body.webhookUrl?.trim() ?? "";
983
- if (raw !== "" && !/^https:\/\/.+/.test(raw)) {
1347
+ const hasWebhookUpdate = body.webhookUrl !== undefined;
1348
+ const raw = hasWebhookUpdate
1349
+ ? (body.webhookUrl?.trim() ?? "")
1350
+ : undefined;
1351
+ if (raw !== undefined && raw !== "" && !/^https:\/\/.+/.test(raw)) {
984
1352
  res.writeHead(400, { "Content-Type": "application/json" });
985
1353
  res.end(JSON.stringify({ error: "webhookUrl must be HTTPS" }));
986
1354
  return;
@@ -1002,16 +1370,69 @@ export class Server extends EventEmitter {
1002
1370
  port: cfg.dashboard?.port ?? 3000,
1003
1371
  requireApproval: cfg.dashboard?.requireApproval ?? ["high"],
1004
1372
  pushNotifications: cfg.dashboard?.pushNotifications ?? false,
1005
- webhookUrl: raw || undefined,
1373
+ webhookUrl: hasWebhookUpdate
1374
+ ? raw || undefined
1375
+ : cfg.dashboard?.webhookUrl,
1006
1376
  };
1007
1377
  if (gateRaw !== undefined) {
1008
1378
  cfg.approvalGate = gateRaw;
1009
1379
  this.approvalGate = gateRaw;
1010
1380
  }
1381
+ const driverRaw = body.driver;
1382
+ if (driverRaw !== undefined) {
1383
+ const validDrivers = [
1384
+ "subprocess",
1385
+ "api",
1386
+ "openai",
1387
+ "grok",
1388
+ "gemini",
1389
+ "none",
1390
+ ];
1391
+ if (!validDrivers.includes(driverRaw)) {
1392
+ res.writeHead(400, { "Content-Type": "application/json" });
1393
+ res.end(JSON.stringify({
1394
+ error: `driver must be one of: ${validDrivers.join(", ")}`,
1395
+ }));
1396
+ return;
1397
+ }
1398
+ const driver = driverRaw;
1399
+ cfg.driver = driver;
1400
+ saveBridgeConfigDriver(driver, this.bridgeConfigPath);
1401
+ }
1402
+ if (body.apiKey) {
1403
+ const { provider, key } = body.apiKey;
1404
+ const validProviders = ["anthropic", "openai", "google", "xai"];
1405
+ if (!validProviders.includes(provider) ||
1406
+ typeof key !== "string") {
1407
+ res.writeHead(400, { "Content-Type": "application/json" });
1408
+ res.end(JSON.stringify({ error: "Invalid apiKey provider or key" }));
1409
+ return;
1410
+ }
1411
+ cfg.apiKeys = { ...cfg.apiKeys, [provider]: key || undefined };
1412
+ }
1011
1413
  savePatchworkConfig(cfg, configPath);
1012
- this.approvalWebhookUrl = raw || undefined;
1414
+ if (hasWebhookUpdate) {
1415
+ this.approvalWebhookUrl = raw || undefined;
1416
+ }
1417
+ if (body.pushServiceUrl !== undefined) {
1418
+ const pushUrl = body.pushServiceUrl.trim();
1419
+ if (pushUrl && !pushUrl.startsWith("https://")) {
1420
+ res.writeHead(400, { "Content-Type": "application/json" });
1421
+ res.end(JSON.stringify({ error: "pushServiceUrl must be HTTPS" }));
1422
+ return;
1423
+ }
1424
+ this.pushServiceUrl = pushUrl || undefined;
1425
+ }
1426
+ if (body.pushServiceToken !== undefined) {
1427
+ this.pushServiceToken = body.pushServiceToken.trim() || undefined;
1428
+ }
1429
+ if (body.pushServiceBaseUrl !== undefined) {
1430
+ this.pushServiceBaseUrl =
1431
+ body.pushServiceBaseUrl.trim() || undefined;
1432
+ }
1433
+ const restartRequired = driverRaw !== undefined || body.apiKey !== undefined;
1013
1434
  res.writeHead(200, { "Content-Type": "application/json" });
1014
- res.end(JSON.stringify({ ok: true }));
1435
+ res.end(JSON.stringify({ ok: true, restartRequired }));
1015
1436
  }
1016
1437
  catch (err) {
1017
1438
  res.writeHead(400, { "Content-Type": "application/json" });
@@ -1100,6 +1521,7 @@ export class Server extends EventEmitter {
1100
1521
  path: parsedUrl.pathname,
1101
1522
  body: parsedBody,
1102
1523
  query: parsedUrl.searchParams,
1524
+ approvalToken: req.headers["x-approval-token"],
1103
1525
  }, {
1104
1526
  queue: getApprovalQueue(),
1105
1527
  workspace: process.cwd(),
@@ -1107,6 +1529,9 @@ export class Server extends EventEmitter {
1107
1529
  onDecision: this.onApprovalDecision,
1108
1530
  webhookUrl: this.approvalWebhookUrl,
1109
1531
  approvalGate: this.approvalGate,
1532
+ pushServiceUrl: this.pushServiceUrl,
1533
+ pushServiceToken: this.pushServiceToken,
1534
+ pushServiceBaseUrl: this.pushServiceBaseUrl,
1110
1535
  });
1111
1536
  res.writeHead(result.status, {
1112
1537
  "Content-Type": "application/json",
@@ -1305,6 +1730,8 @@ export class Server extends EventEmitter {
1305
1730
  ws.on("error", (err) => {
1306
1731
  this.logger.error(`WebSocket client error: ${err.message}`);
1307
1732
  });
1733
+ ws.remoteAddr =
1734
+ req.socket.remoteAddress;
1308
1735
  this.emit("connection", ws);
1309
1736
  });
1310
1737
  }