patchwork-os 0.2.0-alpha.0 → 0.2.0-alpha.11

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 (136) hide show
  1. package/README.md +41 -46
  2. package/dist/bridge.js +23 -10
  3. package/dist/bridge.js.map +1 -1
  4. package/dist/claudeDriver.d.ts +3 -1
  5. package/dist/claudeDriver.js +48 -0
  6. package/dist/claudeDriver.js.map +1 -1
  7. package/dist/commands/dashboard.d.ts +47 -0
  8. package/dist/commands/dashboard.js +319 -0
  9. package/dist/commands/dashboard.js.map +1 -0
  10. package/dist/config.d.ts +2 -2
  11. package/dist/config.js +5 -2
  12. package/dist/config.js.map +1 -1
  13. package/dist/connectors/github.d.ts +94 -0
  14. package/dist/connectors/github.js +350 -0
  15. package/dist/connectors/github.js.map +1 -0
  16. package/dist/connectors/gmail.d.ts +40 -0
  17. package/dist/connectors/gmail.js +304 -0
  18. package/dist/connectors/gmail.js.map +1 -0
  19. package/dist/connectors/googleCalendar.d.ts +57 -0
  20. package/dist/connectors/googleCalendar.js +308 -0
  21. package/dist/connectors/googleCalendar.js.map +1 -0
  22. package/dist/connectors/linear.d.ts +117 -0
  23. package/dist/connectors/linear.js +248 -0
  24. package/dist/connectors/linear.js.map +1 -0
  25. package/dist/connectors/mcpClient.d.ts +56 -0
  26. package/dist/connectors/mcpClient.js +189 -0
  27. package/dist/connectors/mcpClient.js.map +1 -0
  28. package/dist/connectors/mcpOAuth.d.ts +83 -0
  29. package/dist/connectors/mcpOAuth.js +363 -0
  30. package/dist/connectors/mcpOAuth.js.map +1 -0
  31. package/dist/connectors/sentry.d.ts +43 -0
  32. package/dist/connectors/sentry.js +197 -0
  33. package/dist/connectors/sentry.js.map +1 -0
  34. package/dist/connectors/slack.d.ts +50 -0
  35. package/dist/connectors/slack.js +254 -0
  36. package/dist/connectors/slack.js.map +1 -0
  37. package/dist/drivers/claude/api.d.ts +11 -0
  38. package/dist/drivers/claude/api.js +54 -0
  39. package/dist/drivers/claude/api.js.map +1 -0
  40. package/dist/drivers/claude/envSanitizer.d.ts +7 -0
  41. package/dist/drivers/claude/envSanitizer.js +18 -0
  42. package/dist/drivers/claude/envSanitizer.js.map +1 -0
  43. package/dist/drivers/claude/streamParser.d.ts +38 -0
  44. package/dist/drivers/claude/streamParser.js +34 -0
  45. package/dist/drivers/claude/streamParser.js.map +1 -0
  46. package/dist/drivers/claude/subprocess.d.ts +19 -0
  47. package/dist/drivers/claude/subprocess.js +216 -0
  48. package/dist/drivers/claude/subprocess.js.map +1 -0
  49. package/dist/drivers/claude/subprocessSettings.d.ts +9 -0
  50. package/dist/drivers/claude/subprocessSettings.js +55 -0
  51. package/dist/drivers/claude/subprocessSettings.js.map +1 -0
  52. package/dist/drivers/gemini/index.d.ts +14 -0
  53. package/dist/drivers/gemini/index.js +176 -0
  54. package/dist/drivers/gemini/index.js.map +1 -0
  55. package/dist/drivers/grok/index.d.ts +11 -0
  56. package/dist/drivers/grok/index.js +22 -0
  57. package/dist/drivers/grok/index.js.map +1 -0
  58. package/dist/drivers/index.d.ts +18 -0
  59. package/dist/drivers/index.js +31 -0
  60. package/dist/drivers/index.js.map +1 -0
  61. package/dist/drivers/openai/index.d.ts +24 -0
  62. package/dist/drivers/openai/index.js +110 -0
  63. package/dist/drivers/openai/index.js.map +1 -0
  64. package/dist/drivers/types.d.ts +72 -0
  65. package/dist/drivers/types.js +30 -0
  66. package/dist/drivers/types.js.map +1 -0
  67. package/dist/index.js +116 -22
  68. package/dist/index.js.map +1 -1
  69. package/dist/recipes/yamlRunner.d.ts +104 -0
  70. package/dist/recipes/yamlRunner.js +683 -0
  71. package/dist/recipes/yamlRunner.js.map +1 -0
  72. package/dist/recipesHttp.d.ts +13 -1
  73. package/dist/recipesHttp.js +9 -1
  74. package/dist/recipesHttp.js.map +1 -1
  75. package/dist/runLog.d.ts +5 -0
  76. package/dist/runLog.js +44 -0
  77. package/dist/runLog.js.map +1 -1
  78. package/dist/server.d.ts +3 -1
  79. package/dist/server.js +490 -2
  80. package/dist/server.js.map +1 -1
  81. package/dist/tools/addLinearComment.d.ts +55 -0
  82. package/dist/tools/addLinearComment.js +70 -0
  83. package/dist/tools/addLinearComment.js.map +1 -0
  84. package/dist/tools/createLinearIssue.d.ts +84 -0
  85. package/dist/tools/createLinearIssue.js +146 -0
  86. package/dist/tools/createLinearIssue.js.map +1 -0
  87. package/dist/tools/ctxGetTaskContext.d.ts +4 -1
  88. package/dist/tools/ctxGetTaskContext.js +45 -2
  89. package/dist/tools/ctxGetTaskContext.js.map +1 -1
  90. package/dist/tools/fetchCalendarEvents.d.ts +94 -0
  91. package/dist/tools/fetchCalendarEvents.js +97 -0
  92. package/dist/tools/fetchCalendarEvents.js.map +1 -0
  93. package/dist/tools/fetchGithubIssue.d.ts +80 -0
  94. package/dist/tools/fetchGithubIssue.js +84 -0
  95. package/dist/tools/fetchGithubIssue.js.map +1 -0
  96. package/dist/tools/fetchGithubPR.d.ts +89 -0
  97. package/dist/tools/fetchGithubPR.js +96 -0
  98. package/dist/tools/fetchGithubPR.js.map +1 -0
  99. package/dist/tools/fetchLinearIssue.d.ts +112 -0
  100. package/dist/tools/fetchLinearIssue.js +129 -0
  101. package/dist/tools/fetchLinearIssue.js.map +1 -0
  102. package/dist/tools/fetchSentryIssue.d.ts +143 -0
  103. package/dist/tools/fetchSentryIssue.js +150 -0
  104. package/dist/tools/fetchSentryIssue.js.map +1 -0
  105. package/dist/tools/fetchSlackProfile.d.ts +43 -0
  106. package/dist/tools/fetchSlackProfile.js +43 -0
  107. package/dist/tools/fetchSlackProfile.js.map +1 -0
  108. package/dist/tools/getConnectorStatus.d.ts +58 -0
  109. package/dist/tools/getConnectorStatus.js +56 -0
  110. package/dist/tools/getConnectorStatus.js.map +1 -0
  111. package/dist/tools/github/index.d.ts +1 -1
  112. package/dist/tools/github/index.js +1 -1
  113. package/dist/tools/github/index.js.map +1 -1
  114. package/dist/tools/github/pr.d.ts +122 -0
  115. package/dist/tools/github/pr.js +152 -0
  116. package/dist/tools/github/pr.js.map +1 -1
  117. package/dist/tools/index.js +27 -1
  118. package/dist/tools/index.js.map +1 -1
  119. package/dist/tools/slackListChannels.d.ts +65 -0
  120. package/dist/tools/slackListChannels.js +70 -0
  121. package/dist/tools/slackListChannels.js.map +1 -0
  122. package/dist/tools/slackPostMessage.d.ts +57 -0
  123. package/dist/tools/slackPostMessage.js +72 -0
  124. package/dist/tools/slackPostMessage.js.map +1 -0
  125. package/dist/tools/updateLinearIssue.d.ts +89 -0
  126. package/dist/tools/updateLinearIssue.js +103 -0
  127. package/dist/tools/updateLinearIssue.js.map +1 -0
  128. package/package.json +1 -1
  129. package/scripts/start-all.sh +56 -19
  130. package/templates/recipes/ctx-loop-test.yaml +75 -0
  131. package/templates/recipes/gmail-health-check.yaml +19 -0
  132. package/templates/recipes/inbox-triage.yaml +15 -0
  133. package/templates/recipes/morning-brief-slack.yaml +54 -0
  134. package/templates/recipes/morning-brief.yaml +72 -0
  135. package/templates/recipes/sentry-to-linear.yaml +77 -0
  136. package/templates/scheduled-tasks/morning-brief/SKILL.md +37 -0
package/dist/server.js CHANGED
@@ -1,5 +1,7 @@
1
1
  import { EventEmitter } from "node:events";
2
2
  import http from "node:http";
3
+ import os from "node:os";
4
+ import path from "node:path";
3
5
  import { WebSocket, WebSocketServer as WsServer } from "ws";
4
6
  import { routeApprovalRequest } from "./approvalHttp.js";
5
7
  import { getApprovalQueue } from "./approvalQueue.js";
@@ -353,6 +355,80 @@ export class Server extends EventEmitter {
353
355
  res.end(JSON.stringify({ ok: true, v: PACKAGE_VERSION }));
354
356
  return;
355
357
  }
358
+ // ── Connector OAuth callbacks (unauthenticated — browser redirect from vendor) ──
359
+ if (parsedUrl.pathname === "/connections/github/callback" &&
360
+ req.method === "GET") {
361
+ void (async () => {
362
+ const { handleGithubCallback } = await import("./connectors/github.js");
363
+ const code = parsedUrl.searchParams.get("code");
364
+ const state = parsedUrl.searchParams.get("state");
365
+ const error = parsedUrl.searchParams.get("error");
366
+ const result = await handleGithubCallback(code, state, error);
367
+ res.writeHead(result.status, {
368
+ "Content-Type": result.contentType ?? "application/json",
369
+ });
370
+ res.end(result.body);
371
+ })();
372
+ return;
373
+ }
374
+ if (parsedUrl.pathname === "/connections/linear/callback" &&
375
+ req.method === "GET") {
376
+ void (async () => {
377
+ const { handleLinearCallback } = await import("./connectors/linear.js");
378
+ const code = parsedUrl.searchParams.get("code");
379
+ const state = parsedUrl.searchParams.get("state");
380
+ const error = parsedUrl.searchParams.get("error");
381
+ const result = await handleLinearCallback(code, state, error);
382
+ res.writeHead(result.status, {
383
+ "Content-Type": result.contentType ?? "application/json",
384
+ });
385
+ res.end(result.body);
386
+ })();
387
+ return;
388
+ }
389
+ if (parsedUrl.pathname === "/connections/sentry/callback" &&
390
+ req.method === "GET") {
391
+ void (async () => {
392
+ const { handleSentryCallback } = await import("./connectors/sentry.js");
393
+ const code = parsedUrl.searchParams.get("code");
394
+ const state = parsedUrl.searchParams.get("state");
395
+ const error = parsedUrl.searchParams.get("error");
396
+ const result = await handleSentryCallback(code, state, error);
397
+ res.writeHead(result.status, {
398
+ "Content-Type": result.contentType ?? "application/json",
399
+ });
400
+ res.end(result.body);
401
+ })();
402
+ return;
403
+ }
404
+ if (parsedUrl.pathname === "/connections/google-calendar/callback" &&
405
+ req.method === "GET") {
406
+ void (async () => {
407
+ const { handleCalendarCallback } = await import("./connectors/googleCalendar.js");
408
+ const code = parsedUrl.searchParams.get("code");
409
+ const state = parsedUrl.searchParams.get("state");
410
+ const error = parsedUrl.searchParams.get("error");
411
+ const result = await handleCalendarCallback(code, state, error);
412
+ res.writeHead(result.status, {
413
+ "Content-Type": result.contentType ?? "application/json",
414
+ });
415
+ res.end(result.body);
416
+ })();
417
+ return;
418
+ }
419
+ if (parsedUrl.pathname === "/connections/slack/callback" &&
420
+ req.method === "GET") {
421
+ void (async () => {
422
+ const { handleSlackCallback } = await import("./connectors/slack.js");
423
+ const code = parsedUrl.searchParams.get("code");
424
+ const state = parsedUrl.searchParams.get("state");
425
+ const error = parsedUrl.searchParams.get("error");
426
+ const result = await handleSlackCallback(code, state, error);
427
+ res.writeHead(result.status, { "Content-Type": result.contentType ?? "application/json" });
428
+ res.end(result.body);
429
+ })();
430
+ return;
431
+ }
356
432
  // ── Bearer token authentication ───────────────────────────────────────
357
433
  // All other HTTP endpoints require a valid Bearer token.
358
434
  // Accepts either:
@@ -643,6 +719,411 @@ export class Server extends EventEmitter {
643
719
  });
644
720
  return;
645
721
  }
722
+ // ── Gmail / Connections endpoints ───────────────────────────────────────
723
+ if (parsedUrl.pathname === "/connections" && req.method === "GET") {
724
+ void (async () => {
725
+ const { handleConnectionsList } = await import("./connectors/gmail.js");
726
+ const result = await handleConnectionsList();
727
+ res.writeHead(result.status, {
728
+ "Content-Type": result.contentType ?? "application/json",
729
+ });
730
+ res.end(result.body);
731
+ })();
732
+ return;
733
+ }
734
+ if (parsedUrl.pathname === "/connections/gmail/auth" &&
735
+ req.method === "GET") {
736
+ void (async () => {
737
+ const { handleGmailAuthRedirect } = await import("./connectors/gmail.js");
738
+ const result = handleGmailAuthRedirect();
739
+ if (result.redirect) {
740
+ res.writeHead(302, { Location: result.redirect });
741
+ res.end();
742
+ }
743
+ else {
744
+ res.writeHead(result.status, {
745
+ "Content-Type": result.contentType ?? "application/json",
746
+ });
747
+ res.end(result.body);
748
+ }
749
+ })();
750
+ return;
751
+ }
752
+ if (parsedUrl.pathname === "/connections/gmail/callback" &&
753
+ req.method === "GET") {
754
+ void (async () => {
755
+ const { handleGmailCallback } = await import("./connectors/gmail.js");
756
+ const code = parsedUrl.searchParams.get("code");
757
+ const state = parsedUrl.searchParams.get("state");
758
+ const error = parsedUrl.searchParams.get("error");
759
+ const result = await handleGmailCallback(code, state, error);
760
+ res.writeHead(result.status, {
761
+ "Content-Type": result.contentType ?? "text/html",
762
+ });
763
+ res.end(result.body);
764
+ })();
765
+ return;
766
+ }
767
+ if (parsedUrl.pathname === "/connections/gmail" &&
768
+ req.method === "DELETE") {
769
+ void (async () => {
770
+ const { handleGmailDisconnect } = await import("./connectors/gmail.js");
771
+ const result = await handleGmailDisconnect();
772
+ res.writeHead(result.status, {
773
+ "Content-Type": result.contentType ?? "application/json",
774
+ });
775
+ res.end(result.body);
776
+ })();
777
+ return;
778
+ }
779
+ if (parsedUrl.pathname === "/connections/gmail/test" &&
780
+ req.method === "POST") {
781
+ void (async () => {
782
+ const { handleGmailTest } = await import("./connectors/gmail.js");
783
+ const result = await handleGmailTest();
784
+ res.writeHead(result.status, {
785
+ "Content-Type": result.contentType ?? "application/json",
786
+ });
787
+ res.end(result.body);
788
+ })();
789
+ return;
790
+ }
791
+ // ── GitHub MCP connector routes ─────────────────────────────────────
792
+ if (parsedUrl.pathname === "/connections/github/auth" &&
793
+ req.method === "GET") {
794
+ void (async () => {
795
+ const { handleGithubAuthorize } = await import("./connectors/github.js");
796
+ const result = await handleGithubAuthorize();
797
+ if (result.redirect) {
798
+ res.writeHead(302, { Location: result.redirect });
799
+ res.end();
800
+ }
801
+ else {
802
+ res.writeHead(result.status, {
803
+ "Content-Type": result.contentType ?? "application/json",
804
+ });
805
+ res.end(result.body);
806
+ }
807
+ })();
808
+ return;
809
+ }
810
+ if (parsedUrl.pathname === "/connections/github/test" &&
811
+ req.method === "POST") {
812
+ void (async () => {
813
+ const { handleGithubTest } = await import("./connectors/github.js");
814
+ const result = await handleGithubTest();
815
+ res.writeHead(result.status, {
816
+ "Content-Type": result.contentType ?? "application/json",
817
+ });
818
+ res.end(result.body);
819
+ })();
820
+ return;
821
+ }
822
+ if (parsedUrl.pathname === "/connections/github" &&
823
+ req.method === "DELETE") {
824
+ void (async () => {
825
+ const { handleGithubDisconnect } = await import("./connectors/github.js");
826
+ const result = await handleGithubDisconnect();
827
+ res.writeHead(result.status, {
828
+ "Content-Type": result.contentType ?? "application/json",
829
+ });
830
+ res.end(result.body);
831
+ })();
832
+ return;
833
+ }
834
+ // ── Sentry MCP connector routes ─────────────────────────────────────
835
+ if (parsedUrl.pathname === "/connections/sentry/auth" &&
836
+ req.method === "GET") {
837
+ void (async () => {
838
+ const { handleSentryAuthorize } = await import("./connectors/sentry.js");
839
+ const result = await handleSentryAuthorize();
840
+ if (result.redirect) {
841
+ res.writeHead(302, { Location: result.redirect });
842
+ res.end();
843
+ }
844
+ else {
845
+ res.writeHead(result.status, {
846
+ "Content-Type": result.contentType ?? "application/json",
847
+ });
848
+ res.end(result.body);
849
+ }
850
+ })();
851
+ return;
852
+ }
853
+ if (parsedUrl.pathname === "/connections/sentry/callback" &&
854
+ req.method === "GET") {
855
+ void (async () => {
856
+ const { handleSentryCallback } = await import("./connectors/sentry.js");
857
+ const code = parsedUrl.searchParams.get("code");
858
+ const state = parsedUrl.searchParams.get("state");
859
+ const error = parsedUrl.searchParams.get("error");
860
+ const result = await handleSentryCallback(code, state, error);
861
+ res.writeHead(result.status, {
862
+ "Content-Type": result.contentType ?? "application/json",
863
+ });
864
+ res.end(result.body);
865
+ })();
866
+ return;
867
+ }
868
+ if (parsedUrl.pathname === "/connections/sentry/test" &&
869
+ req.method === "POST") {
870
+ void (async () => {
871
+ const { handleSentryTest } = await import("./connectors/sentry.js");
872
+ const result = await handleSentryTest();
873
+ res.writeHead(result.status, {
874
+ "Content-Type": result.contentType ?? "application/json",
875
+ });
876
+ res.end(result.body);
877
+ })();
878
+ return;
879
+ }
880
+ if (parsedUrl.pathname === "/connections/sentry" &&
881
+ req.method === "DELETE") {
882
+ void (async () => {
883
+ const { handleSentryDisconnect } = await import("./connectors/sentry.js");
884
+ const result = await handleSentryDisconnect();
885
+ res.writeHead(result.status, {
886
+ "Content-Type": result.contentType ?? "application/json",
887
+ });
888
+ res.end(result.body);
889
+ })();
890
+ return;
891
+ }
892
+ // ── Linear MCP connector routes ─────────────────────────────────────
893
+ if (parsedUrl.pathname === "/connections/linear/auth" &&
894
+ req.method === "GET") {
895
+ void (async () => {
896
+ const { handleLinearAuthorize } = await import("./connectors/linear.js");
897
+ const result = await handleLinearAuthorize();
898
+ if (result.redirect) {
899
+ res.writeHead(302, { Location: result.redirect });
900
+ res.end();
901
+ }
902
+ else {
903
+ res.writeHead(result.status, {
904
+ "Content-Type": result.contentType ?? "application/json",
905
+ });
906
+ res.end(result.body);
907
+ }
908
+ })();
909
+ return;
910
+ }
911
+ if (parsedUrl.pathname === "/connections/linear/callback" &&
912
+ req.method === "GET") {
913
+ void (async () => {
914
+ const { handleLinearCallback } = await import("./connectors/linear.js");
915
+ const code = parsedUrl.searchParams.get("code");
916
+ const state = parsedUrl.searchParams.get("state");
917
+ const error = parsedUrl.searchParams.get("error");
918
+ const result = await handleLinearCallback(code, state, error);
919
+ res.writeHead(result.status, {
920
+ "Content-Type": result.contentType ?? "application/json",
921
+ });
922
+ res.end(result.body);
923
+ })();
924
+ return;
925
+ }
926
+ if (parsedUrl.pathname === "/connections/linear/test" &&
927
+ req.method === "POST") {
928
+ void (async () => {
929
+ const { handleLinearTest } = await import("./connectors/linear.js");
930
+ const result = await handleLinearTest();
931
+ res.writeHead(result.status, {
932
+ "Content-Type": result.contentType ?? "application/json",
933
+ });
934
+ res.end(result.body);
935
+ })();
936
+ return;
937
+ }
938
+ if (parsedUrl.pathname === "/connections/linear" &&
939
+ req.method === "DELETE") {
940
+ void (async () => {
941
+ const { handleLinearDisconnect } = await import("./connectors/linear.js");
942
+ const result = await handleLinearDisconnect();
943
+ res.writeHead(result.status, {
944
+ "Content-Type": result.contentType ?? "application/json",
945
+ });
946
+ res.end(result.body);
947
+ })();
948
+ return;
949
+ }
950
+ // ── Slack connector routes ──────────────────────────────────────
951
+ if ((parsedUrl.pathname === "/connections/slack/auth" || parsedUrl.pathname === "/connections/slack/authorize") &&
952
+ req.method === "GET") {
953
+ const { handleSlackAuthorize } = await import("./connectors/slack.js");
954
+ const result = handleSlackAuthorize();
955
+ if (result.redirect) {
956
+ res.writeHead(302, { Location: result.redirect });
957
+ res.end();
958
+ }
959
+ else {
960
+ res.writeHead(result.status, { "Content-Type": result.contentType ?? "application/json" });
961
+ res.end(result.body);
962
+ }
963
+ return;
964
+ }
965
+ if (parsedUrl.pathname === "/connections/slack/test" &&
966
+ req.method === "POST") {
967
+ void (async () => {
968
+ const { handleSlackTest } = await import("./connectors/slack.js");
969
+ const result = await handleSlackTest();
970
+ res.writeHead(result.status, { "Content-Type": result.contentType ?? "application/json" });
971
+ res.end(result.body);
972
+ })();
973
+ return;
974
+ }
975
+ if (parsedUrl.pathname === "/connections/slack" &&
976
+ req.method === "DELETE") {
977
+ const { handleSlackDisconnect } = await import("./connectors/slack.js");
978
+ const result = handleSlackDisconnect();
979
+ res.writeHead(result.status, { "Content-Type": result.contentType ?? "application/json" });
980
+ res.end(result.body);
981
+ return;
982
+ }
983
+ // ── Google Calendar routes ──────────────────────────────────────
984
+ if (parsedUrl.pathname === "/connections/google-calendar/auth" &&
985
+ req.method === "GET") {
986
+ void (async () => {
987
+ const { handleCalendarAuthRedirect } = await import("./connectors/googleCalendar.js");
988
+ const result = handleCalendarAuthRedirect();
989
+ if (result.redirect) {
990
+ res.writeHead(302, { Location: result.redirect });
991
+ res.end();
992
+ }
993
+ else {
994
+ res.writeHead(result.status, {
995
+ "Content-Type": result.contentType ?? "application/json",
996
+ });
997
+ res.end(result.body);
998
+ }
999
+ })();
1000
+ return;
1001
+ }
1002
+ if (parsedUrl.pathname === "/connections/google-calendar/callback" &&
1003
+ req.method === "GET") {
1004
+ void (async () => {
1005
+ const { handleCalendarCallback } = await import("./connectors/googleCalendar.js");
1006
+ const code = parsedUrl.searchParams.get("code");
1007
+ const state = parsedUrl.searchParams.get("state");
1008
+ const error = parsedUrl.searchParams.get("error");
1009
+ const result = await handleCalendarCallback(code, state, error);
1010
+ res.writeHead(result.status, {
1011
+ "Content-Type": result.contentType ?? "application/json",
1012
+ });
1013
+ res.end(result.body);
1014
+ })();
1015
+ return;
1016
+ }
1017
+ if (parsedUrl.pathname === "/connections/google-calendar/test" &&
1018
+ req.method === "POST") {
1019
+ void (async () => {
1020
+ const { handleCalendarTest } = await import("./connectors/googleCalendar.js");
1021
+ const result = await handleCalendarTest();
1022
+ res.writeHead(result.status, {
1023
+ "Content-Type": result.contentType ?? "application/json",
1024
+ });
1025
+ res.end(result.body);
1026
+ })();
1027
+ return;
1028
+ }
1029
+ if (parsedUrl.pathname === "/connections/google-calendar" &&
1030
+ req.method === "DELETE") {
1031
+ void (async () => {
1032
+ const { handleCalendarDisconnect } = await import("./connectors/googleCalendar.js");
1033
+ const result = await handleCalendarDisconnect();
1034
+ res.writeHead(result.status, {
1035
+ "Content-Type": result.contentType ?? "application/json",
1036
+ });
1037
+ res.end(result.body);
1038
+ })();
1039
+ return;
1040
+ }
1041
+ // ── Inbox routes ────────────────────────────────────────────────────
1042
+ if (parsedUrl.pathname === "/inbox" && req.method === "GET") {
1043
+ void (async () => {
1044
+ try {
1045
+ const { readdir, readFile, stat } = await import("node:fs/promises");
1046
+ const { existsSync } = await import("node:fs");
1047
+ const inboxDir = path.join(os.homedir(), ".patchwork", "inbox");
1048
+ if (!existsSync(inboxDir)) {
1049
+ res.writeHead(200, { "Content-Type": "application/json" });
1050
+ res.end(JSON.stringify({ items: [] }));
1051
+ return;
1052
+ }
1053
+ const files = (await readdir(inboxDir)).filter((f) => f.endsWith(".md"));
1054
+ const items = await Promise.all(files.map(async (name) => {
1055
+ const filePath = path.join(inboxDir, name);
1056
+ const [content, stats] = await Promise.all([
1057
+ readFile(filePath, "utf8"),
1058
+ stat(filePath),
1059
+ ]);
1060
+ const stripped = content
1061
+ .split("\n")
1062
+ .filter((l) => !l.startsWith("#"))
1063
+ .join("\n")
1064
+ .trim();
1065
+ return {
1066
+ name,
1067
+ path: filePath,
1068
+ modifiedAt: stats.mtime.toISOString(),
1069
+ preview: stripped.slice(0, 200),
1070
+ };
1071
+ }));
1072
+ items.sort((a, b) => new Date(b.modifiedAt).getTime() -
1073
+ new Date(a.modifiedAt).getTime());
1074
+ res.writeHead(200, { "Content-Type": "application/json" });
1075
+ res.end(JSON.stringify({ items }));
1076
+ }
1077
+ catch (err) {
1078
+ res.writeHead(500, { "Content-Type": "application/json" });
1079
+ res.end(JSON.stringify({
1080
+ error: err instanceof Error ? err.message : String(err),
1081
+ }));
1082
+ }
1083
+ })();
1084
+ return;
1085
+ }
1086
+ const inboxFileMatch = parsedUrl.pathname?.match(/^\/inbox\/([^/]+\.md)$/);
1087
+ if (inboxFileMatch && req.method === "GET") {
1088
+ void (async () => {
1089
+ try {
1090
+ const { readFile, stat } = await import("node:fs/promises");
1091
+ const filename = decodeURIComponent(inboxFileMatch[1] ?? "");
1092
+ // Prevent path traversal — filename must not contain directory separators
1093
+ if (filename.includes("/") || filename.includes("\\")) {
1094
+ res.writeHead(400, { "Content-Type": "application/json" });
1095
+ res.end(JSON.stringify({ error: "Invalid filename" }));
1096
+ return;
1097
+ }
1098
+ const filePath = path.join(os.homedir(), ".patchwork", "inbox", filename);
1099
+ const [content, stats] = await Promise.all([
1100
+ readFile(filePath, "utf8"),
1101
+ stat(filePath),
1102
+ ]);
1103
+ res.writeHead(200, { "Content-Type": "application/json" });
1104
+ res.end(JSON.stringify({
1105
+ name: filename,
1106
+ content,
1107
+ modifiedAt: stats.mtime.toISOString(),
1108
+ }));
1109
+ }
1110
+ catch (err) {
1111
+ const code = err.code;
1112
+ if (code === "ENOENT") {
1113
+ res.writeHead(404, { "Content-Type": "application/json" });
1114
+ res.end(JSON.stringify({ error: "Not found" }));
1115
+ }
1116
+ else {
1117
+ res.writeHead(500, { "Content-Type": "application/json" });
1118
+ res.end(JSON.stringify({
1119
+ error: err instanceof Error ? err.message : String(err),
1120
+ }));
1121
+ }
1122
+ }
1123
+ })();
1124
+ return;
1125
+ }
1126
+ // ── End inbox routes ─────────────────────────────────────────────────
646
1127
  if (parsedUrl.pathname === "/recipes/run" && req.method === "POST") {
647
1128
  const chunks = [];
648
1129
  req.on("data", (c) => chunks.push(c));
@@ -652,6 +1133,11 @@ export class Server extends EventEmitter {
652
1133
  const body = Buffer.concat(chunks).toString("utf-8");
653
1134
  const parsed = JSON.parse(body || "{}");
654
1135
  const name = parsed.name;
1136
+ const vars = parsed.vars &&
1137
+ typeof parsed.vars === "object" &&
1138
+ !Array.isArray(parsed.vars)
1139
+ ? parsed.vars
1140
+ : undefined;
655
1141
  if (typeof name !== "string" || !name) {
656
1142
  res.writeHead(400, { "Content-Type": "application/json" });
657
1143
  res.end(JSON.stringify({ ok: false, error: "name required" }));
@@ -665,7 +1151,7 @@ export class Server extends EventEmitter {
665
1151
  }));
666
1152
  return;
667
1153
  }
668
- const result = await this.runRecipeFn(name);
1154
+ const result = await this.runRecipeFn(name, vars);
669
1155
  res.writeHead(result.ok ? 200 : 400, {
670
1156
  "Content-Type": "application/json",
671
1157
  });
@@ -759,7 +1245,7 @@ export class Server extends EventEmitter {
759
1245
  const id = sessionDetailMatch[1];
760
1246
  try {
761
1247
  const data = this.sessionDetailFn?.(id);
762
- if (!data || !data.summary) {
1248
+ if (!data?.summary) {
763
1249
  res.writeHead(404, { "Content-Type": "application/json" });
764
1250
  res.end(JSON.stringify({ error: "unknown sessionId" }));
765
1251
  return;
@@ -1126,6 +1612,8 @@ export class Server extends EventEmitter {
1126
1612
  ws.on("error", (err) => {
1127
1613
  this.logger.error(`WebSocket client error: ${err.message}`);
1128
1614
  });
1615
+ ws.remoteAddr =
1616
+ req.socket.remoteAddress;
1129
1617
  this.emit("connection", ws);
1130
1618
  });
1131
1619
  }