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.
- package/README.bridge.md +6 -0
- package/README.md +13 -2
- package/dist/approvalHttp.d.ts +11 -2
- package/dist/approvalHttp.js +92 -9
- package/dist/approvalHttp.js.map +1 -1
- package/dist/approvalQueue.d.ts +12 -1
- package/dist/approvalQueue.js +25 -3
- package/dist/approvalQueue.js.map +1 -1
- package/dist/bridge.js +127 -23
- package/dist/bridge.js.map +1 -1
- package/dist/claudeDriver.d.ts +3 -1
- package/dist/claudeDriver.js +48 -0
- package/dist/claudeDriver.js.map +1 -1
- package/dist/claudeOrchestrator.d.ts +1 -1
- package/dist/claudeOrchestrator.js +14 -8
- package/dist/claudeOrchestrator.js.map +1 -1
- package/dist/commands/launchd.d.ts +2 -0
- package/dist/commands/launchd.js +94 -0
- package/dist/commands/launchd.js.map +1 -0
- package/dist/config.d.ts +7 -2
- package/dist/config.js +85 -8
- package/dist/config.js.map +1 -1
- package/dist/connectors/github.d.ts +58 -8
- package/dist/connectors/github.js +321 -84
- package/dist/connectors/github.js.map +1 -1
- package/dist/connectors/gmail.d.ts +4 -1
- package/dist/connectors/gmail.js +77 -16
- package/dist/connectors/gmail.js.map +1 -1
- package/dist/connectors/googleCalendar.d.ts +60 -0
- package/dist/connectors/googleCalendar.js +329 -0
- package/dist/connectors/googleCalendar.js.map +1 -0
- package/dist/connectors/linear.d.ts +117 -0
- package/dist/connectors/linear.js +248 -0
- package/dist/connectors/linear.js.map +1 -0
- package/dist/connectors/mcpClient.d.ts +56 -0
- package/dist/connectors/mcpClient.js +189 -0
- package/dist/connectors/mcpClient.js.map +1 -0
- package/dist/connectors/mcpOAuth.d.ts +83 -0
- package/dist/connectors/mcpOAuth.js +363 -0
- package/dist/connectors/mcpOAuth.js.map +1 -0
- package/dist/connectors/sentry.d.ts +43 -0
- package/dist/connectors/sentry.js +197 -0
- package/dist/connectors/sentry.js.map +1 -0
- package/dist/connectors/slack.d.ts +50 -0
- package/dist/connectors/slack.js +289 -0
- package/dist/connectors/slack.js.map +1 -0
- package/dist/drivers/claude/api.d.ts +11 -0
- package/dist/drivers/claude/api.js +54 -0
- package/dist/drivers/claude/api.js.map +1 -0
- package/dist/drivers/claude/envSanitizer.d.ts +7 -0
- package/dist/drivers/claude/envSanitizer.js +18 -0
- package/dist/drivers/claude/envSanitizer.js.map +1 -0
- package/dist/drivers/claude/streamParser.d.ts +38 -0
- package/dist/drivers/claude/streamParser.js +34 -0
- package/dist/drivers/claude/streamParser.js.map +1 -0
- package/dist/drivers/claude/subprocess.d.ts +19 -0
- package/dist/drivers/claude/subprocess.js +216 -0
- package/dist/drivers/claude/subprocess.js.map +1 -0
- package/dist/drivers/claude/subprocessSettings.d.ts +9 -0
- package/dist/drivers/claude/subprocessSettings.js +55 -0
- package/dist/drivers/claude/subprocessSettings.js.map +1 -0
- package/dist/drivers/gemini/index.d.ts +18 -0
- package/dist/drivers/gemini/index.js +210 -0
- package/dist/drivers/gemini/index.js.map +1 -0
- package/dist/drivers/grok/index.d.ts +11 -0
- package/dist/drivers/grok/index.js +22 -0
- package/dist/drivers/grok/index.js.map +1 -0
- package/dist/drivers/index.d.ts +23 -0
- package/dist/drivers/index.js +31 -0
- package/dist/drivers/index.js.map +1 -0
- package/dist/drivers/openai/index.d.ts +24 -0
- package/dist/drivers/openai/index.js +110 -0
- package/dist/drivers/openai/index.js.map +1 -0
- package/dist/drivers/types.d.ts +72 -0
- package/dist/drivers/types.js +30 -0
- package/dist/drivers/types.js.map +1 -0
- package/dist/index.js +35 -1
- package/dist/index.js.map +1 -1
- package/dist/installGuard.d.ts +25 -0
- package/dist/installGuard.js +48 -0
- package/dist/installGuard.js.map +1 -0
- package/dist/patchworkConfig.d.ts +9 -0
- package/dist/patchworkConfig.js.map +1 -1
- package/dist/recipes/scheduler.d.ts +23 -7
- package/dist/recipes/scheduler.js +135 -41
- package/dist/recipes/scheduler.js.map +1 -1
- package/dist/recipes/yamlRunner.d.ts +15 -0
- package/dist/recipes/yamlRunner.js +325 -26
- package/dist/recipes/yamlRunner.js.map +1 -1
- package/dist/recipesHttp.d.ts +14 -1
- package/dist/recipesHttp.js +21 -4
- package/dist/recipesHttp.js.map +1 -1
- package/dist/runLog.d.ts +5 -0
- package/dist/runLog.js +51 -1
- package/dist/runLog.js.map +1 -1
- package/dist/server.d.ts +15 -1
- package/dist/server.js +458 -31
- package/dist/server.js.map +1 -1
- package/dist/tools/addLinearComment.d.ts +55 -0
- package/dist/tools/addLinearComment.js +72 -0
- package/dist/tools/addLinearComment.js.map +1 -0
- package/dist/tools/bridgeDoctor.js +2 -2
- package/dist/tools/bridgeDoctor.js.map +1 -1
- package/dist/tools/createLinearIssue.d.ts +84 -0
- package/dist/tools/createLinearIssue.js +146 -0
- package/dist/tools/createLinearIssue.js.map +1 -0
- package/dist/tools/ctxGetTaskContext.d.ts +4 -1
- package/dist/tools/ctxGetTaskContext.js +45 -2
- package/dist/tools/ctxGetTaskContext.js.map +1 -1
- package/dist/tools/fetchCalendarEvents.d.ts +94 -0
- package/dist/tools/fetchCalendarEvents.js +97 -0
- package/dist/tools/fetchCalendarEvents.js.map +1 -0
- package/dist/tools/fetchGithubIssue.d.ts +80 -0
- package/dist/tools/fetchGithubIssue.js +84 -0
- package/dist/tools/fetchGithubIssue.js.map +1 -0
- package/dist/tools/fetchGithubPR.d.ts +89 -0
- package/dist/tools/fetchGithubPR.js +96 -0
- package/dist/tools/fetchGithubPR.js.map +1 -0
- package/dist/tools/fetchLinearIssue.d.ts +112 -0
- package/dist/tools/fetchLinearIssue.js +129 -0
- package/dist/tools/fetchLinearIssue.js.map +1 -0
- package/dist/tools/fetchSentryIssue.d.ts +143 -0
- package/dist/tools/fetchSentryIssue.js +150 -0
- package/dist/tools/fetchSentryIssue.js.map +1 -0
- package/dist/tools/fetchSlackProfile.d.ts +43 -0
- package/dist/tools/fetchSlackProfile.js +46 -0
- package/dist/tools/fetchSlackProfile.js.map +1 -0
- package/dist/tools/getConnectorStatus.d.ts +58 -0
- package/dist/tools/getConnectorStatus.js +56 -0
- package/dist/tools/getConnectorStatus.js.map +1 -0
- package/dist/tools/github/index.d.ts +1 -1
- package/dist/tools/github/index.js +1 -1
- package/dist/tools/github/index.js.map +1 -1
- package/dist/tools/github/pr.d.ts +122 -0
- package/dist/tools/github/pr.js +183 -0
- package/dist/tools/github/pr.js.map +1 -1
- package/dist/tools/index.js +27 -1
- package/dist/tools/index.js.map +1 -1
- package/dist/tools/slackListChannels.d.ts +65 -0
- package/dist/tools/slackListChannels.js +70 -0
- package/dist/tools/slackListChannels.js.map +1 -0
- package/dist/tools/slackPostMessage.d.ts +57 -0
- package/dist/tools/slackPostMessage.js +77 -0
- package/dist/tools/slackPostMessage.js.map +1 -0
- package/dist/tools/updateLinearIssue.d.ts +89 -0
- package/dist/tools/updateLinearIssue.js +117 -0
- package/dist/tools/updateLinearIssue.js.map +1 -0
- package/package.json +4 -2
- package/scripts/start-all.sh +56 -19
- package/templates/co.patchwork-os.bridge.plist +34 -0
- package/templates/recipes/ctx-loop-test.yaml +75 -0
- package/templates/recipes/morning-brief-slack.yaml +57 -0
- package/templates/recipes/morning-brief.yaml +21 -5
- 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
|
|
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 {
|
|
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
|
|
877
|
+
const result = await handleSentryCallback(code, state, error);
|
|
686
878
|
res.writeHead(result.status, {
|
|
687
|
-
"Content-Type": result.contentType ?? "
|
|
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/
|
|
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 {
|
|
697
|
-
const result = await
|
|
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
|
-
|
|
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 {
|
|
709
|
-
const result = await
|
|
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/
|
|
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 {
|
|
721
|
-
const
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
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(
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
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
|
|
983
|
-
|
|
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:
|
|
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
|
-
|
|
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
|
}
|