patchwork-os 0.2.0-alpha.3 → 0.2.0-alpha.31
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 +40 -15
- package/deploy/bootstrap-vps.sh +184 -0
- package/deploy/deploy-dashboard.sh +174 -0
- package/deploy/deploy-landing.sh +79 -0
- package/dist/activationMetrics.d.ts +67 -0
- package/dist/activationMetrics.js +255 -0
- package/dist/activationMetrics.js.map +1 -0
- package/dist/approvalHttp.d.ts +24 -2
- package/dist/approvalHttp.js +150 -10
- package/dist/approvalHttp.js.map +1 -1
- package/dist/approvalQueue.d.ts +16 -1
- package/dist/approvalQueue.js +44 -3
- package/dist/approvalQueue.js.map +1 -1
- package/dist/automation.d.ts +20 -0
- package/dist/automation.js +54 -1
- package/dist/automation.js.map +1 -1
- package/dist/bridge.d.ts +2 -0
- package/dist/bridge.js +55 -130
- package/dist/bridge.js.map +1 -1
- package/dist/bridgeToken.js +57 -19
- package/dist/bridgeToken.js.map +1 -1
- package/dist/ccPermissions.js +6 -4
- package/dist/ccPermissions.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/commands/recipe.d.ts +258 -0
- package/dist/commands/recipe.js +1130 -0
- package/dist/commands/recipe.js.map +1 -0
- package/dist/commands/recipeInstall.d.ts +72 -0
- package/dist/commands/recipeInstall.js +339 -0
- package/dist/commands/recipeInstall.js.map +1 -0
- package/dist/config.d.ts +14 -1
- package/dist/config.js +99 -8
- package/dist/config.js.map +1 -1
- package/dist/connectors/baseConnector.d.ts +117 -0
- package/dist/connectors/baseConnector.js +213 -0
- package/dist/connectors/baseConnector.js.map +1 -0
- package/dist/connectors/confluence.d.ts +111 -0
- package/dist/connectors/confluence.js +406 -0
- package/dist/connectors/confluence.js.map +1 -0
- package/dist/connectors/datadog.d.ts +116 -0
- package/dist/connectors/datadog.js +385 -0
- package/dist/connectors/datadog.js.map +1 -0
- package/dist/connectors/fixtureLibrary.d.ts +21 -0
- package/dist/connectors/fixtureLibrary.js +70 -0
- package/dist/connectors/fixtureLibrary.js.map +1 -0
- package/dist/connectors/fixtureRecorder.d.ts +1 -0
- package/dist/connectors/fixtureRecorder.js +35 -0
- package/dist/connectors/fixtureRecorder.js.map +1 -0
- package/dist/connectors/github.d.ts +58 -8
- package/dist/connectors/github.js +312 -84
- package/dist/connectors/github.js.map +1 -1
- package/dist/connectors/gmail.d.ts +4 -1
- package/dist/connectors/gmail.js +79 -16
- package/dist/connectors/gmail.js.map +1 -1
- package/dist/connectors/googleCalendar.d.ts +60 -0
- package/dist/connectors/googleCalendar.js +345 -0
- package/dist/connectors/googleCalendar.js.map +1 -0
- package/dist/connectors/hubspot.d.ts +112 -0
- package/dist/connectors/hubspot.js +408 -0
- package/dist/connectors/hubspot.js.map +1 -0
- package/dist/connectors/intercom.d.ts +102 -0
- package/dist/connectors/intercom.js +402 -0
- package/dist/connectors/intercom.js.map +1 -0
- package/dist/connectors/jira.d.ts +98 -0
- package/dist/connectors/jira.js +379 -0
- package/dist/connectors/jira.js.map +1 -0
- package/dist/connectors/linear.d.ts +69 -19
- package/dist/connectors/linear.js +170 -129
- package/dist/connectors/linear.js.map +1 -1
- 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 +84 -0
- package/dist/connectors/mcpOAuth.js +389 -0
- package/dist/connectors/mcpOAuth.js.map +1 -0
- package/dist/connectors/mockConnector.d.ts +28 -0
- package/dist/connectors/mockConnector.js +81 -0
- package/dist/connectors/mockConnector.js.map +1 -0
- package/dist/connectors/notion.d.ts +143 -0
- package/dist/connectors/notion.js +424 -0
- package/dist/connectors/notion.js.map +1 -0
- package/dist/connectors/sentry.d.ts +17 -21
- package/dist/connectors/sentry.js +115 -131
- package/dist/connectors/sentry.js.map +1 -1
- package/dist/connectors/slack.d.ts +50 -0
- package/dist/connectors/slack.js +324 -0
- package/dist/connectors/slack.js.map +1 -0
- package/dist/connectors/stripe.d.ts +116 -0
- package/dist/connectors/stripe.js +379 -0
- package/dist/connectors/stripe.js.map +1 -0
- package/dist/connectors/tokenStorage.d.ts +35 -0
- package/dist/connectors/tokenStorage.js +459 -0
- package/dist/connectors/tokenStorage.js.map +1 -0
- package/dist/connectors/zendesk.d.ts +104 -0
- package/dist/connectors/zendesk.js +424 -0
- package/dist/connectors/zendesk.js.map +1 -0
- package/dist/drivers/gemini/index.d.ts +5 -1
- package/dist/drivers/gemini/index.js +39 -5
- package/dist/drivers/gemini/index.js.map +1 -1
- package/dist/drivers/index.d.ts +5 -0
- package/dist/drivers/index.js +1 -1
- package/dist/drivers/index.js.map +1 -1
- package/dist/featureFlags.d.ts +73 -0
- package/dist/featureFlags.js +203 -0
- package/dist/featureFlags.js.map +1 -0
- package/dist/fp/automationInterpreter.js +1 -0
- package/dist/fp/automationInterpreter.js.map +1 -1
- package/dist/fp/automationProgram.d.ts +1 -1
- package/dist/fp/automationProgram.js.map +1 -1
- package/dist/fp/policyParser.js +17 -0
- package/dist/fp/policyParser.js.map +1 -1
- package/dist/index.js +621 -61
- 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/oauth.d.ts +4 -1
- package/dist/oauth.js +50 -14
- package/dist/oauth.js.map +1 -1
- package/dist/patchworkConfig.d.ts +9 -0
- package/dist/patchworkConfig.js.map +1 -1
- package/dist/recipeOrchestration.d.ts +53 -0
- package/dist/recipeOrchestration.js +272 -0
- package/dist/recipeOrchestration.js.map +1 -0
- package/dist/recipes/RecipeOrchestrator.d.ts +40 -0
- package/dist/recipes/RecipeOrchestrator.js +51 -0
- package/dist/recipes/RecipeOrchestrator.js.map +1 -0
- package/dist/recipes/agentExecutor.d.ts +28 -0
- package/dist/recipes/agentExecutor.js +42 -0
- package/dist/recipes/agentExecutor.js.map +1 -0
- package/dist/recipes/chainedRunner.d.ts +140 -0
- package/dist/recipes/chainedRunner.js +539 -0
- package/dist/recipes/chainedRunner.js.map +1 -0
- package/dist/recipes/dependencyGraph.d.ts +39 -0
- package/dist/recipes/dependencyGraph.js +199 -0
- package/dist/recipes/dependencyGraph.js.map +1 -0
- package/dist/recipes/legacyRecipeCompat.d.ts +2 -0
- package/dist/recipes/legacyRecipeCompat.js +112 -0
- package/dist/recipes/legacyRecipeCompat.js.map +1 -0
- package/dist/recipes/manifest.d.ts +47 -0
- package/dist/recipes/manifest.js +141 -0
- package/dist/recipes/manifest.js.map +1 -0
- package/dist/recipes/nestedRecipeStep.d.ts +58 -0
- package/dist/recipes/nestedRecipeStep.js +95 -0
- package/dist/recipes/nestedRecipeStep.js.map +1 -0
- package/dist/recipes/outputRegistry.d.ts +28 -0
- package/dist/recipes/outputRegistry.js +52 -0
- package/dist/recipes/outputRegistry.js.map +1 -0
- package/dist/recipes/scheduler.d.ts +23 -7
- package/dist/recipes/scheduler.js +131 -41
- package/dist/recipes/scheduler.js.map +1 -1
- package/dist/recipes/schema.d.ts +17 -2
- package/dist/recipes/schemaGenerator.d.ts +28 -0
- package/dist/recipes/schemaGenerator.js +565 -0
- package/dist/recipes/schemaGenerator.js.map +1 -0
- package/dist/recipes/templateEngine.d.ts +62 -0
- package/dist/recipes/templateEngine.js +182 -0
- package/dist/recipes/templateEngine.js.map +1 -0
- package/dist/recipes/toolRegistry.d.ts +181 -0
- package/dist/recipes/toolRegistry.js +300 -0
- package/dist/recipes/toolRegistry.js.map +1 -0
- package/dist/recipes/tools/calendar.d.ts +6 -0
- package/dist/recipes/tools/calendar.js +61 -0
- package/dist/recipes/tools/calendar.js.map +1 -0
- package/dist/recipes/tools/confluence.d.ts +6 -0
- package/dist/recipes/tools/confluence.js +254 -0
- package/dist/recipes/tools/confluence.js.map +1 -0
- package/dist/recipes/tools/datadog.d.ts +6 -0
- package/dist/recipes/tools/datadog.js +239 -0
- package/dist/recipes/tools/datadog.js.map +1 -0
- package/dist/recipes/tools/diagnostics.d.ts +6 -0
- package/dist/recipes/tools/diagnostics.js +36 -0
- package/dist/recipes/tools/diagnostics.js.map +1 -0
- package/dist/recipes/tools/file.d.ts +6 -0
- package/dist/recipes/tools/file.js +170 -0
- package/dist/recipes/tools/file.js.map +1 -0
- package/dist/recipes/tools/git.d.ts +6 -0
- package/dist/recipes/tools/git.js +63 -0
- package/dist/recipes/tools/git.js.map +1 -0
- package/dist/recipes/tools/github.d.ts +6 -0
- package/dist/recipes/tools/github.js +91 -0
- package/dist/recipes/tools/github.js.map +1 -0
- package/dist/recipes/tools/gmail.d.ts +6 -0
- package/dist/recipes/tools/gmail.js +210 -0
- package/dist/recipes/tools/gmail.js.map +1 -0
- package/dist/recipes/tools/hubspot.d.ts +6 -0
- package/dist/recipes/tools/hubspot.js +232 -0
- package/dist/recipes/tools/hubspot.js.map +1 -0
- package/dist/recipes/tools/index.d.ts +22 -0
- package/dist/recipes/tools/index.js +25 -0
- package/dist/recipes/tools/index.js.map +1 -0
- package/dist/recipes/tools/intercom.d.ts +6 -0
- package/dist/recipes/tools/intercom.js +226 -0
- package/dist/recipes/tools/intercom.js.map +1 -0
- package/dist/recipes/tools/linear.d.ts +6 -0
- package/dist/recipes/tools/linear.js +83 -0
- package/dist/recipes/tools/linear.js.map +1 -0
- package/dist/recipes/tools/notion.d.ts +6 -0
- package/dist/recipes/tools/notion.js +278 -0
- package/dist/recipes/tools/notion.js.map +1 -0
- package/dist/recipes/tools/slack.d.ts +6 -0
- package/dist/recipes/tools/slack.js +72 -0
- package/dist/recipes/tools/slack.js.map +1 -0
- package/dist/recipes/tools/stripe.d.ts +6 -0
- package/dist/recipes/tools/stripe.js +265 -0
- package/dist/recipes/tools/stripe.js.map +1 -0
- package/dist/recipes/tools/zendesk.d.ts +6 -0
- package/dist/recipes/tools/zendesk.js +245 -0
- package/dist/recipes/tools/zendesk.js.map +1 -0
- package/dist/recipes/validation.d.ts +13 -0
- package/dist/recipes/validation.js +433 -0
- package/dist/recipes/validation.js.map +1 -0
- package/dist/recipes/yamlRunner.d.ts +87 -0
- package/dist/recipes/yamlRunner.js +693 -409
- package/dist/recipes/yamlRunner.js.map +1 -1
- package/dist/recipesHttp.d.ts +34 -6
- package/dist/recipesHttp.js +285 -15
- package/dist/recipesHttp.js.map +1 -1
- package/dist/riskTier.js +1 -0
- package/dist/riskTier.js.map +1 -1
- package/dist/runLog.d.ts +23 -0
- package/dist/runLog.js +56 -1
- package/dist/runLog.js.map +1 -1
- package/dist/schemas/dry-run-plan.v1.json +139 -0
- package/dist/schemas/recipe.v1.json +684 -0
- package/dist/server.d.ts +32 -1
- package/dist/server.js +980 -97
- package/dist/server.js.map +1 -1
- package/dist/streamableHttp.js +2 -0
- package/dist/streamableHttp.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/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/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/actions.js +4 -2
- package/dist/tools/github/actions.js.map +1 -1
- package/dist/tools/github/composite.d.ts +339 -0
- package/dist/tools/github/composite.js +343 -0
- package/dist/tools/github/composite.js.map +1 -0
- package/dist/tools/github/index.d.ts +2 -1
- package/dist/tools/github/index.js +2 -1
- package/dist/tools/github/index.js.map +1 -1
- package/dist/tools/github/issues.js +8 -4
- package/dist/tools/github/issues.js.map +1 -1
- package/dist/tools/github/pr.d.ts +122 -0
- package/dist/tools/github/pr.js +195 -5
- package/dist/tools/github/pr.js.map +1 -1
- package/dist/tools/index.js +32 -1
- package/dist/tools/index.js.map +1 -1
- package/dist/tools/searchTools.js +1 -1
- package/dist/tools/searchTools.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/testTraceToSource.js +2 -2
- package/dist/tools/testTraceToSource.js.map +1 -1
- package/dist/tools/updateLinearIssue.d.ts +89 -0
- package/dist/tools/updateLinearIssue.js +117 -0
- package/dist/tools/updateLinearIssue.js.map +1 -0
- package/dist/transport.d.ts +7 -1
- package/dist/transport.js +85 -11
- package/dist/transport.js.map +1 -1
- package/package.json +5 -2
- package/scripts/start-all.sh +56 -19
- package/templates/automation-policies/recipe-authoring.json +25 -0
- package/templates/automation-policy.example.json +6 -0
- package/templates/co.patchwork-os.bridge.plist +34 -0
- package/templates/recipes/ctx-loop-test.yaml +75 -0
- package/templates/recipes/lint-on-save.yaml +1 -2
- package/templates/recipes/morning-brief-slack.yaml +57 -0
- package/templates/recipes/morning-brief.yaml +14 -6
- package/templates/recipes/project-health-check.yaml +50 -0
- package/templates/recipes/sentry-to-linear.yaml +77 -0
package/dist/server.js
CHANGED
|
@@ -3,8 +3,10 @@ import http from "node:http";
|
|
|
3
3
|
import os from "node:os";
|
|
4
4
|
import path from "node:path";
|
|
5
5
|
import { WebSocket, WebSocketServer as WsServer } from "ws";
|
|
6
|
-
import {
|
|
6
|
+
import { computeSummary as computeActivationSummary, loadMetrics as loadActivationMetrics, } from "./activationMetrics.js";
|
|
7
|
+
import { handleApprovalsStream, routeApprovalRequest } from "./approvalHttp.js";
|
|
7
8
|
import { getApprovalQueue } from "./approvalQueue.js";
|
|
9
|
+
import { saveBridgeConfigDriver } from "./config.js";
|
|
8
10
|
import { timingSafeStringEqual } from "./crypto.js";
|
|
9
11
|
import { renderDashboardHtml } from "./dashboard.js";
|
|
10
12
|
import { loadConfig as loadPatchworkConfig, defaultConfigPath as patchworkConfigPath, saveConfig as savePatchworkConfig, } from "./patchworkConfig.js";
|
|
@@ -87,20 +89,38 @@ export class Server extends EventEmitter {
|
|
|
87
89
|
readyFn = null;
|
|
88
90
|
/** Set by bridge to provide task list data (sanitized — no raw prompts) */
|
|
89
91
|
tasksFn = null;
|
|
92
|
+
/** Set by bridge to cancel a running/pending task by id. Returns true if found. */
|
|
93
|
+
cancelTaskFn = null;
|
|
90
94
|
/** Patchwork: set by bridge to list installed recipes for the dashboard. */
|
|
91
95
|
recipesFn = null;
|
|
96
|
+
/** Patchwork: set by bridge to load raw recipe source content by name. */
|
|
97
|
+
loadRecipeContentFn = null;
|
|
98
|
+
/** Patchwork: set by bridge to save raw recipe source content by name. */
|
|
99
|
+
saveRecipeContentFn = null;
|
|
92
100
|
/** Patchwork: set by bridge to save a new recipe draft to disk. */
|
|
93
101
|
saveRecipeFn = null;
|
|
94
102
|
/** Patchwork: set by bridge to query the recipe run audit log. */
|
|
95
103
|
runsFn = null;
|
|
104
|
+
/** Patchwork: set by bridge to fetch a single run by seq for the detail page. */
|
|
105
|
+
runDetailFn = null;
|
|
106
|
+
/** Patchwork: set by bridge to generate a dry-run plan for a recipe by name. */
|
|
107
|
+
runPlanFn = null;
|
|
96
108
|
/** Patchwork: set by bridge to launch a named recipe via the orchestrator. */
|
|
97
109
|
runRecipeFn = null;
|
|
98
110
|
/** Patchwork: admin-controlled managed settings path (highest rule precedence). */
|
|
99
111
|
managedSettingsPath = undefined;
|
|
112
|
+
/** Effective bridge config path to update when dashboard saves driver changes. */
|
|
113
|
+
bridgeConfigPath = undefined;
|
|
100
114
|
/** Patchwork: live approval gate level — mutated by POST /settings, read by bridge per-session setup. */
|
|
101
115
|
approvalGate = "off";
|
|
102
116
|
/** Patchwork: outbound webhook URL for approval notifications (from dashboard.webhookUrl in config). */
|
|
103
117
|
approvalWebhookUrl = undefined;
|
|
118
|
+
/** Patchwork: push relay service URL — when set, per-callId approval tokens are generated. */
|
|
119
|
+
pushServiceUrl = undefined;
|
|
120
|
+
/** Patchwork: bearer token for the push relay service. */
|
|
121
|
+
pushServiceToken = undefined;
|
|
122
|
+
/** Patchwork: public base URL of this bridge, embedded in push payloads as callback base. */
|
|
123
|
+
pushServiceBaseUrl = undefined;
|
|
104
124
|
/** Patchwork: approval decision audit callback wired to activityLog.recordEvent. */
|
|
105
125
|
onApprovalDecision = undefined;
|
|
106
126
|
/** Patchwork: set by bridge to match + fire webhook-triggered recipes. */
|
|
@@ -127,6 +147,7 @@ export class Server extends EventEmitter {
|
|
|
127
147
|
sessionDetailFn = null;
|
|
128
148
|
/** Set by bridge to handle POST /launch-quick-task — invokes launchQuickTask tool in-process. */
|
|
129
149
|
launchQuickTaskFn = null;
|
|
150
|
+
setRecipeEnabledFn = null;
|
|
130
151
|
/**
|
|
131
152
|
* Attach an OAuth 2.0 Authorization Server.
|
|
132
153
|
* When set, the bridge exposes:
|
|
@@ -355,6 +376,144 @@ export class Server extends EventEmitter {
|
|
|
355
376
|
res.end(JSON.stringify({ ok: true, v: PACKAGE_VERSION }));
|
|
356
377
|
return;
|
|
357
378
|
}
|
|
379
|
+
// ── Connector OAuth callbacks (unauthenticated — browser redirect from vendor) ──
|
|
380
|
+
if (parsedUrl.pathname === "/connections/github/callback" &&
|
|
381
|
+
req.method === "GET") {
|
|
382
|
+
void (async () => {
|
|
383
|
+
const { handleGithubCallback } = await import("./connectors/github.js");
|
|
384
|
+
const code = parsedUrl.searchParams.get("code");
|
|
385
|
+
const state = parsedUrl.searchParams.get("state");
|
|
386
|
+
const error = parsedUrl.searchParams.get("error");
|
|
387
|
+
const result = await handleGithubCallback(code, state, error);
|
|
388
|
+
res.writeHead(result.status, {
|
|
389
|
+
"Content-Type": result.contentType ?? "application/json",
|
|
390
|
+
});
|
|
391
|
+
res.end(result.body);
|
|
392
|
+
})();
|
|
393
|
+
return;
|
|
394
|
+
}
|
|
395
|
+
if (parsedUrl.pathname === "/connections/linear/callback" &&
|
|
396
|
+
req.method === "GET") {
|
|
397
|
+
void (async () => {
|
|
398
|
+
const { handleLinearCallback } = await import("./connectors/linear.js");
|
|
399
|
+
const code = parsedUrl.searchParams.get("code");
|
|
400
|
+
const state = parsedUrl.searchParams.get("state");
|
|
401
|
+
const error = parsedUrl.searchParams.get("error");
|
|
402
|
+
const result = await handleLinearCallback(code, state, error);
|
|
403
|
+
res.writeHead(result.status, {
|
|
404
|
+
"Content-Type": result.contentType ?? "application/json",
|
|
405
|
+
});
|
|
406
|
+
res.end(result.body);
|
|
407
|
+
})();
|
|
408
|
+
return;
|
|
409
|
+
}
|
|
410
|
+
if (parsedUrl.pathname === "/connections/sentry/callback" &&
|
|
411
|
+
req.method === "GET") {
|
|
412
|
+
void (async () => {
|
|
413
|
+
const { handleSentryCallback } = await import("./connectors/sentry.js");
|
|
414
|
+
const code = parsedUrl.searchParams.get("code");
|
|
415
|
+
const state = parsedUrl.searchParams.get("state");
|
|
416
|
+
const error = parsedUrl.searchParams.get("error");
|
|
417
|
+
const result = await handleSentryCallback(code, state, error);
|
|
418
|
+
res.writeHead(result.status, {
|
|
419
|
+
"Content-Type": result.contentType ?? "application/json",
|
|
420
|
+
});
|
|
421
|
+
res.end(result.body);
|
|
422
|
+
})();
|
|
423
|
+
return;
|
|
424
|
+
}
|
|
425
|
+
if (parsedUrl.pathname === "/connections/google-calendar/callback" &&
|
|
426
|
+
req.method === "GET") {
|
|
427
|
+
void (async () => {
|
|
428
|
+
const { handleCalendarCallback } = await import("./connectors/googleCalendar.js");
|
|
429
|
+
const code = parsedUrl.searchParams.get("code");
|
|
430
|
+
const state = parsedUrl.searchParams.get("state");
|
|
431
|
+
const error = parsedUrl.searchParams.get("error");
|
|
432
|
+
const result = await handleCalendarCallback(code, state, error);
|
|
433
|
+
res.writeHead(result.status, {
|
|
434
|
+
"Content-Type": result.contentType ?? "application/json",
|
|
435
|
+
});
|
|
436
|
+
res.end(result.body);
|
|
437
|
+
})();
|
|
438
|
+
return;
|
|
439
|
+
}
|
|
440
|
+
if (parsedUrl.pathname === "/connections/slack/callback" &&
|
|
441
|
+
req.method === "GET") {
|
|
442
|
+
void (async () => {
|
|
443
|
+
const { handleSlackCallback } = await import("./connectors/slack.js");
|
|
444
|
+
const code = parsedUrl.searchParams.get("code");
|
|
445
|
+
const state = parsedUrl.searchParams.get("state");
|
|
446
|
+
const error = parsedUrl.searchParams.get("error");
|
|
447
|
+
const result = await handleSlackCallback(code, state, error);
|
|
448
|
+
res.writeHead(result.status, {
|
|
449
|
+
"Content-Type": result.contentType ?? "application/json",
|
|
450
|
+
});
|
|
451
|
+
res.end(result.body);
|
|
452
|
+
})();
|
|
453
|
+
return;
|
|
454
|
+
}
|
|
455
|
+
if (parsedUrl.pathname === "/connections/gmail/callback" &&
|
|
456
|
+
req.method === "GET") {
|
|
457
|
+
void (async () => {
|
|
458
|
+
const { handleGmailCallback } = await import("./connectors/gmail.js");
|
|
459
|
+
const code = parsedUrl.searchParams.get("code");
|
|
460
|
+
const state = parsedUrl.searchParams.get("state");
|
|
461
|
+
const error = parsedUrl.searchParams.get("error");
|
|
462
|
+
const result = await handleGmailCallback(code, state, error);
|
|
463
|
+
res.writeHead(result.status, {
|
|
464
|
+
"Content-Type": result.contentType ?? "text/html",
|
|
465
|
+
});
|
|
466
|
+
res.end(result.body);
|
|
467
|
+
})();
|
|
468
|
+
return;
|
|
469
|
+
}
|
|
470
|
+
// ── /schemas/* — unauthenticated registry-derived JSON Schemas ────────
|
|
471
|
+
// Serves recipe.v1.json, dry-run-plan.v1.json, tools/<ns>.json so YAML-LSP
|
|
472
|
+
// editors can resolve `$schema:` headers against a running bridge. No
|
|
473
|
+
// secrets — schemas are generated from the tool registry.
|
|
474
|
+
if (parsedUrl.pathname?.startsWith("/schemas/") && req.method === "GET") {
|
|
475
|
+
try {
|
|
476
|
+
await import("./recipes/tools/index.js");
|
|
477
|
+
const { generateSchemaSet } = await import("./recipes/schemaGenerator.js");
|
|
478
|
+
const schemas = generateSchemaSet();
|
|
479
|
+
const rest = parsedUrl.pathname.slice("/schemas/".length);
|
|
480
|
+
let body;
|
|
481
|
+
if (rest === "recipe.v1.json") {
|
|
482
|
+
body = schemas.recipe;
|
|
483
|
+
}
|
|
484
|
+
else if (rest === "dry-run-plan.v1.json") {
|
|
485
|
+
body = schemas.dryRunPlan;
|
|
486
|
+
}
|
|
487
|
+
else if (rest.startsWith("tools/") && rest.endsWith(".json")) {
|
|
488
|
+
const ns = rest.slice("tools/".length, -".json".length);
|
|
489
|
+
body = schemas.namespaces[ns];
|
|
490
|
+
}
|
|
491
|
+
else if (rest === "" || rest === "index.json") {
|
|
492
|
+
body = {
|
|
493
|
+
recipe: "/schemas/recipe.v1.json",
|
|
494
|
+
dryRunPlan: "/schemas/dry-run-plan.v1.json",
|
|
495
|
+
tools: Object.keys(schemas.namespaces).map((ns) => `/schemas/tools/${ns}.json`),
|
|
496
|
+
};
|
|
497
|
+
}
|
|
498
|
+
if (body === undefined) {
|
|
499
|
+
res.writeHead(404, { "Content-Type": "application/json" });
|
|
500
|
+
res.end(JSON.stringify({ error: `schema not found: ${rest}` }));
|
|
501
|
+
return;
|
|
502
|
+
}
|
|
503
|
+
res.writeHead(200, {
|
|
504
|
+
"Content-Type": "application/schema+json",
|
|
505
|
+
"Cache-Control": "public, max-age=60",
|
|
506
|
+
});
|
|
507
|
+
res.end(JSON.stringify(body, null, 2));
|
|
508
|
+
}
|
|
509
|
+
catch (err) {
|
|
510
|
+
res.writeHead(500, { "Content-Type": "application/json" });
|
|
511
|
+
res.end(JSON.stringify({
|
|
512
|
+
error: err instanceof Error ? err.message : String(err),
|
|
513
|
+
}));
|
|
514
|
+
}
|
|
515
|
+
return;
|
|
516
|
+
}
|
|
358
517
|
// ── Bearer token authentication ───────────────────────────────────────
|
|
359
518
|
// All other HTTP endpoints require a valid Bearer token.
|
|
360
519
|
// Accepts either:
|
|
@@ -372,8 +531,13 @@ export class Server extends EventEmitter {
|
|
|
372
531
|
const oauthResolved = !isStaticToken && this.oauthServer
|
|
373
532
|
? this.oauthServer.resolveBearerToken(bearer)
|
|
374
533
|
: null;
|
|
534
|
+
// Phone-path: approve/reject with x-approval-token bypass bearer check.
|
|
535
|
+
// The token itself is validated inside routeApprovalRequest via queue.validateToken.
|
|
536
|
+
const isPhoneApprovalPath = req.method === "POST" &&
|
|
537
|
+
/^\/(approve|reject)\/[A-Za-z0-9-]+$/.test(parsedUrl.pathname) &&
|
|
538
|
+
!!req.headers["x-approval-token"];
|
|
375
539
|
// oauthResolved is the bridge token if the OAuth token is valid; null otherwise
|
|
376
|
-
if (!isStaticToken && !oauthResolved) {
|
|
540
|
+
if (!isStaticToken && !oauthResolved && !isPhoneApprovalPath) {
|
|
377
541
|
// RFC 6750: only include error= when a token was actually presented but invalid
|
|
378
542
|
const tokenPresented = bearer.length > 0;
|
|
379
543
|
const wwwAuth = this.oauthServer && this.oauthIssuerUrl
|
|
@@ -607,6 +771,28 @@ export class Server extends EventEmitter {
|
|
|
607
771
|
}
|
|
608
772
|
return;
|
|
609
773
|
}
|
|
774
|
+
const cancelMatch = parsedUrl.pathname?.match(/^\/tasks\/([^/]+)\/cancel$/);
|
|
775
|
+
if (cancelMatch && req.method === "POST") {
|
|
776
|
+
const taskId = cancelMatch[1];
|
|
777
|
+
try {
|
|
778
|
+
const found = this.cancelTaskFn?.(taskId) ?? false;
|
|
779
|
+
if (!found) {
|
|
780
|
+
res.writeHead(404, { "Content-Type": "application/json" });
|
|
781
|
+
res.end(JSON.stringify({ error: "task not found or already terminal" }));
|
|
782
|
+
}
|
|
783
|
+
else {
|
|
784
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
785
|
+
res.end(JSON.stringify({ ok: true }));
|
|
786
|
+
}
|
|
787
|
+
}
|
|
788
|
+
catch (err) {
|
|
789
|
+
res.writeHead(500, { "Content-Type": "application/json" });
|
|
790
|
+
res.end(JSON.stringify({
|
|
791
|
+
error: err instanceof Error ? err.message : String(err),
|
|
792
|
+
}));
|
|
793
|
+
}
|
|
794
|
+
return;
|
|
795
|
+
}
|
|
610
796
|
if (parsedUrl.pathname?.startsWith("/hooks/") && req.method === "POST") {
|
|
611
797
|
const hookPath = parsedUrl.pathname.substring("/hooks".length);
|
|
612
798
|
const chunks = [];
|
|
@@ -675,21 +861,6 @@ export class Server extends EventEmitter {
|
|
|
675
861
|
})();
|
|
676
862
|
return;
|
|
677
863
|
}
|
|
678
|
-
if (parsedUrl.pathname === "/connections/gmail/callback" &&
|
|
679
|
-
req.method === "GET") {
|
|
680
|
-
void (async () => {
|
|
681
|
-
const { handleGmailCallback } = await import("./connectors/gmail.js");
|
|
682
|
-
const code = parsedUrl.searchParams.get("code");
|
|
683
|
-
const state = parsedUrl.searchParams.get("state");
|
|
684
|
-
const error = parsedUrl.searchParams.get("error");
|
|
685
|
-
const result = await handleGmailCallback(code, state, error);
|
|
686
|
-
res.writeHead(result.status, {
|
|
687
|
-
"Content-Type": result.contentType ?? "text/html",
|
|
688
|
-
});
|
|
689
|
-
res.end(result.body);
|
|
690
|
-
})();
|
|
691
|
-
return;
|
|
692
|
-
}
|
|
693
864
|
if (parsedUrl.pathname === "/connections/gmail" &&
|
|
694
865
|
req.method === "DELETE") {
|
|
695
866
|
void (async () => {
|
|
@@ -714,48 +885,81 @@ export class Server extends EventEmitter {
|
|
|
714
885
|
})();
|
|
715
886
|
return;
|
|
716
887
|
}
|
|
717
|
-
|
|
718
|
-
|
|
888
|
+
// ── GitHub MCP connector routes ─────────────────────────────────────
|
|
889
|
+
if (parsedUrl.pathname === "/connections/github/auth" &&
|
|
890
|
+
req.method === "GET") {
|
|
719
891
|
void (async () => {
|
|
720
|
-
const {
|
|
721
|
-
const
|
|
722
|
-
if (
|
|
723
|
-
res.writeHead(
|
|
724
|
-
res.end(
|
|
725
|
-
ok: true,
|
|
726
|
-
message: `Connected as ${s.user ?? "unknown"}`,
|
|
727
|
-
}));
|
|
892
|
+
const { handleGithubAuthorize } = await import("./connectors/github.js");
|
|
893
|
+
const result = await handleGithubAuthorize();
|
|
894
|
+
if (result.redirect) {
|
|
895
|
+
res.writeHead(302, { Location: result.redirect });
|
|
896
|
+
res.end();
|
|
728
897
|
}
|
|
729
898
|
else {
|
|
730
|
-
res.writeHead(
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
}));
|
|
899
|
+
res.writeHead(result.status, {
|
|
900
|
+
"Content-Type": result.contentType ?? "application/json",
|
|
901
|
+
});
|
|
902
|
+
res.end(result.body);
|
|
735
903
|
}
|
|
736
904
|
})();
|
|
737
905
|
return;
|
|
738
906
|
}
|
|
739
|
-
|
|
740
|
-
if (parsedUrl.pathname === "/connections/sentry/connect" &&
|
|
907
|
+
if (parsedUrl.pathname === "/connections/github/test" &&
|
|
741
908
|
req.method === "POST") {
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
909
|
+
void (async () => {
|
|
910
|
+
const { handleGithubTest } = await import("./connectors/github.js");
|
|
911
|
+
const result = await handleGithubTest();
|
|
912
|
+
res.writeHead(result.status, {
|
|
913
|
+
"Content-Type": result.contentType ?? "application/json",
|
|
914
|
+
});
|
|
915
|
+
res.end(result.body);
|
|
916
|
+
})();
|
|
917
|
+
return;
|
|
918
|
+
}
|
|
919
|
+
if (parsedUrl.pathname === "/connections/github" &&
|
|
920
|
+
req.method === "DELETE") {
|
|
921
|
+
void (async () => {
|
|
922
|
+
const { handleGithubDisconnect } = await import("./connectors/github.js");
|
|
923
|
+
const result = await handleGithubDisconnect();
|
|
924
|
+
res.writeHead(result.status, {
|
|
925
|
+
"Content-Type": result.contentType ?? "application/json",
|
|
926
|
+
});
|
|
927
|
+
res.end(result.body);
|
|
928
|
+
})();
|
|
929
|
+
return;
|
|
930
|
+
}
|
|
931
|
+
// ── Sentry MCP connector routes ─────────────────────────────────────
|
|
932
|
+
if (parsedUrl.pathname === "/connections/sentry/auth" &&
|
|
933
|
+
req.method === "GET") {
|
|
934
|
+
void (async () => {
|
|
935
|
+
const { handleSentryAuthorize } = await import("./connectors/sentry.js");
|
|
936
|
+
const result = await handleSentryAuthorize();
|
|
937
|
+
if (result.redirect) {
|
|
938
|
+
res.writeHead(302, { Location: result.redirect });
|
|
939
|
+
res.end();
|
|
940
|
+
}
|
|
941
|
+
else {
|
|
753
942
|
res.writeHead(result.status, {
|
|
754
943
|
"Content-Type": result.contentType ?? "application/json",
|
|
755
944
|
});
|
|
756
945
|
res.end(result.body);
|
|
757
|
-
}
|
|
758
|
-
});
|
|
946
|
+
}
|
|
947
|
+
})();
|
|
948
|
+
return;
|
|
949
|
+
}
|
|
950
|
+
if (parsedUrl.pathname === "/connections/sentry/callback" &&
|
|
951
|
+
req.method === "GET") {
|
|
952
|
+
void (async () => {
|
|
953
|
+
const { handleSentryCallback } = await import("./connectors/sentry.js");
|
|
954
|
+
const code = parsedUrl.searchParams.get("code");
|
|
955
|
+
const state = parsedUrl.searchParams.get("state");
|
|
956
|
+
const error = parsedUrl.searchParams.get("error");
|
|
957
|
+
const result = await handleSentryCallback(code, state, error);
|
|
958
|
+
res.writeHead(result.status, {
|
|
959
|
+
"Content-Type": result.contentType ?? "application/json",
|
|
960
|
+
});
|
|
961
|
+
res.end(result.body);
|
|
962
|
+
})();
|
|
759
963
|
return;
|
|
760
964
|
}
|
|
761
965
|
if (parsedUrl.pathname === "/connections/sentry/test" &&
|
|
@@ -774,7 +978,7 @@ export class Server extends EventEmitter {
|
|
|
774
978
|
req.method === "DELETE") {
|
|
775
979
|
void (async () => {
|
|
776
980
|
const { handleSentryDisconnect } = await import("./connectors/sentry.js");
|
|
777
|
-
const result = handleSentryDisconnect();
|
|
981
|
+
const result = await handleSentryDisconnect();
|
|
778
982
|
res.writeHead(result.status, {
|
|
779
983
|
"Content-Type": result.contentType ?? "application/json",
|
|
780
984
|
});
|
|
@@ -782,26 +986,38 @@ export class Server extends EventEmitter {
|
|
|
782
986
|
})();
|
|
783
987
|
return;
|
|
784
988
|
}
|
|
785
|
-
// ── Linear connector routes
|
|
786
|
-
if (parsedUrl.pathname === "/connections/linear/
|
|
787
|
-
req.method === "
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
}
|
|
797
|
-
catch { }
|
|
798
|
-
const result = await handleLinearConnect(body);
|
|
989
|
+
// ── Linear MCP connector routes ─────────────────────────────────────
|
|
990
|
+
if (parsedUrl.pathname === "/connections/linear/auth" &&
|
|
991
|
+
req.method === "GET") {
|
|
992
|
+
void (async () => {
|
|
993
|
+
const { handleLinearAuthorize } = await import("./connectors/linear.js");
|
|
994
|
+
const result = await handleLinearAuthorize();
|
|
995
|
+
if (result.redirect) {
|
|
996
|
+
res.writeHead(302, { Location: result.redirect });
|
|
997
|
+
res.end();
|
|
998
|
+
}
|
|
999
|
+
else {
|
|
799
1000
|
res.writeHead(result.status, {
|
|
800
1001
|
"Content-Type": result.contentType ?? "application/json",
|
|
801
1002
|
});
|
|
802
1003
|
res.end(result.body);
|
|
803
|
-
}
|
|
804
|
-
});
|
|
1004
|
+
}
|
|
1005
|
+
})();
|
|
1006
|
+
return;
|
|
1007
|
+
}
|
|
1008
|
+
if (parsedUrl.pathname === "/connections/linear/callback" &&
|
|
1009
|
+
req.method === "GET") {
|
|
1010
|
+
void (async () => {
|
|
1011
|
+
const { handleLinearCallback } = await import("./connectors/linear.js");
|
|
1012
|
+
const code = parsedUrl.searchParams.get("code");
|
|
1013
|
+
const state = parsedUrl.searchParams.get("state");
|
|
1014
|
+
const error = parsedUrl.searchParams.get("error");
|
|
1015
|
+
const result = await handleLinearCallback(code, state, error);
|
|
1016
|
+
res.writeHead(result.status, {
|
|
1017
|
+
"Content-Type": result.contentType ?? "application/json",
|
|
1018
|
+
});
|
|
1019
|
+
res.end(result.body);
|
|
1020
|
+
})();
|
|
805
1021
|
return;
|
|
806
1022
|
}
|
|
807
1023
|
if (parsedUrl.pathname === "/connections/linear/test" &&
|
|
@@ -820,7 +1036,7 @@ export class Server extends EventEmitter {
|
|
|
820
1036
|
req.method === "DELETE") {
|
|
821
1037
|
void (async () => {
|
|
822
1038
|
const { handleLinearDisconnect } = await import("./connectors/linear.js");
|
|
823
|
-
const result = handleLinearDisconnect();
|
|
1039
|
+
const result = await handleLinearDisconnect();
|
|
824
1040
|
res.writeHead(result.status, {
|
|
825
1041
|
"Content-Type": result.contentType ?? "application/json",
|
|
826
1042
|
});
|
|
@@ -828,33 +1044,402 @@ export class Server extends EventEmitter {
|
|
|
828
1044
|
})();
|
|
829
1045
|
return;
|
|
830
1046
|
}
|
|
831
|
-
// ──
|
|
832
|
-
if (parsedUrl.pathname === "/
|
|
1047
|
+
// ── Slack connector routes ──────────────────────────────────────
|
|
1048
|
+
if ((parsedUrl.pathname === "/connections/slack/auth" ||
|
|
1049
|
+
parsedUrl.pathname === "/connections/slack/authorize") &&
|
|
1050
|
+
req.method === "GET") {
|
|
1051
|
+
const { handleSlackAuthorize } = await import("./connectors/slack.js");
|
|
1052
|
+
const result = handleSlackAuthorize();
|
|
1053
|
+
if (result.redirect) {
|
|
1054
|
+
res.writeHead(302, { Location: result.redirect });
|
|
1055
|
+
res.end();
|
|
1056
|
+
}
|
|
1057
|
+
else {
|
|
1058
|
+
res.writeHead(result.status, {
|
|
1059
|
+
"Content-Type": result.contentType ?? "application/json",
|
|
1060
|
+
});
|
|
1061
|
+
res.end(result.body);
|
|
1062
|
+
}
|
|
1063
|
+
return;
|
|
1064
|
+
}
|
|
1065
|
+
if (parsedUrl.pathname === "/connections/slack/test" &&
|
|
1066
|
+
req.method === "POST") {
|
|
833
1067
|
void (async () => {
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
1068
|
+
const { handleSlackTest } = await import("./connectors/slack.js");
|
|
1069
|
+
const result = await handleSlackTest();
|
|
1070
|
+
res.writeHead(result.status, {
|
|
1071
|
+
"Content-Type": result.contentType ?? "application/json",
|
|
1072
|
+
});
|
|
1073
|
+
res.end(result.body);
|
|
1074
|
+
})();
|
|
1075
|
+
return;
|
|
1076
|
+
}
|
|
1077
|
+
if (parsedUrl.pathname === "/connections/slack" &&
|
|
1078
|
+
req.method === "DELETE") {
|
|
1079
|
+
const { handleSlackDisconnect } = await import("./connectors/slack.js");
|
|
1080
|
+
const result = handleSlackDisconnect();
|
|
1081
|
+
res.writeHead(result.status, {
|
|
1082
|
+
"Content-Type": result.contentType ?? "application/json",
|
|
1083
|
+
});
|
|
1084
|
+
res.end(result.body);
|
|
1085
|
+
return;
|
|
1086
|
+
}
|
|
1087
|
+
// ── Notion routes ──────────────────────────────────────────────
|
|
1088
|
+
if (parsedUrl.pathname === "/connections/notion/connect" &&
|
|
1089
|
+
req.method === "POST") {
|
|
1090
|
+
const chunks = [];
|
|
1091
|
+
req.on("data", (c) => chunks.push(c));
|
|
1092
|
+
req.on("end", () => {
|
|
1093
|
+
void (async () => {
|
|
1094
|
+
const { handleNotionConnect } = await import("./connectors/notion.js");
|
|
1095
|
+
const result = await handleNotionConnect(Buffer.concat(chunks).toString("utf-8"));
|
|
1096
|
+
res.writeHead(result.status, {
|
|
1097
|
+
"Content-Type": result.contentType ?? "application/json",
|
|
1098
|
+
});
|
|
1099
|
+
res.end(result.body);
|
|
1100
|
+
})();
|
|
1101
|
+
});
|
|
1102
|
+
return;
|
|
1103
|
+
}
|
|
1104
|
+
if (parsedUrl.pathname === "/connections/notion/test" &&
|
|
1105
|
+
req.method === "POST") {
|
|
1106
|
+
void (async () => {
|
|
1107
|
+
const { handleNotionTest } = await import("./connectors/notion.js");
|
|
1108
|
+
const result = await handleNotionTest();
|
|
1109
|
+
res.writeHead(result.status, {
|
|
1110
|
+
"Content-Type": result.contentType ?? "application/json",
|
|
1111
|
+
});
|
|
1112
|
+
res.end(result.body);
|
|
1113
|
+
})();
|
|
1114
|
+
return;
|
|
1115
|
+
}
|
|
1116
|
+
if (parsedUrl.pathname === "/connections/notion" &&
|
|
1117
|
+
req.method === "DELETE") {
|
|
1118
|
+
const { handleNotionDisconnect } = await import("./connectors/notion.js");
|
|
1119
|
+
const result = handleNotionDisconnect();
|
|
1120
|
+
res.writeHead(result.status, {
|
|
1121
|
+
"Content-Type": result.contentType ?? "application/json",
|
|
1122
|
+
});
|
|
1123
|
+
res.end(result.body);
|
|
1124
|
+
return;
|
|
1125
|
+
}
|
|
1126
|
+
// ── Confluence routes ───────────────────────────────────────────
|
|
1127
|
+
if (parsedUrl.pathname === "/connections/confluence/connect" &&
|
|
1128
|
+
req.method === "POST") {
|
|
1129
|
+
const chunks = [];
|
|
1130
|
+
req.on("data", (c) => chunks.push(c));
|
|
1131
|
+
req.on("end", () => {
|
|
1132
|
+
void (async () => {
|
|
1133
|
+
const { handleConfluenceConnect } = await import("./connectors/confluence.js");
|
|
1134
|
+
const result = await handleConfluenceConnect(Buffer.concat(chunks).toString("utf-8"));
|
|
1135
|
+
res.writeHead(result.status, {
|
|
1136
|
+
"Content-Type": result.contentType ?? "application/json",
|
|
1137
|
+
});
|
|
1138
|
+
res.end(result.body);
|
|
1139
|
+
})();
|
|
1140
|
+
});
|
|
1141
|
+
return;
|
|
1142
|
+
}
|
|
1143
|
+
if (parsedUrl.pathname === "/connections/confluence/test" &&
|
|
1144
|
+
req.method === "POST") {
|
|
1145
|
+
void (async () => {
|
|
1146
|
+
const { handleConfluenceTest } = await import("./connectors/confluence.js");
|
|
1147
|
+
const result = await handleConfluenceTest();
|
|
1148
|
+
res.writeHead(result.status, {
|
|
1149
|
+
"Content-Type": result.contentType ?? "application/json",
|
|
1150
|
+
});
|
|
1151
|
+
res.end(result.body);
|
|
1152
|
+
})();
|
|
1153
|
+
return;
|
|
1154
|
+
}
|
|
1155
|
+
if (parsedUrl.pathname === "/connections/confluence" &&
|
|
1156
|
+
req.method === "DELETE") {
|
|
1157
|
+
const { handleConfluenceDisconnect } = await import("./connectors/confluence.js");
|
|
1158
|
+
const result = handleConfluenceDisconnect();
|
|
1159
|
+
res.writeHead(result.status, {
|
|
1160
|
+
"Content-Type": result.contentType ?? "application/json",
|
|
1161
|
+
});
|
|
1162
|
+
res.end(result.body);
|
|
1163
|
+
return;
|
|
1164
|
+
}
|
|
1165
|
+
// ── Zendesk routes ──────────────────────────────────────────────
|
|
1166
|
+
if (parsedUrl.pathname === "/connections/zendesk/connect" &&
|
|
1167
|
+
req.method === "POST") {
|
|
1168
|
+
const chunks = [];
|
|
1169
|
+
req.on("data", (c) => chunks.push(c));
|
|
1170
|
+
req.on("end", () => {
|
|
1171
|
+
void (async () => {
|
|
1172
|
+
const { handleZendeskConnect } = await import("./connectors/zendesk.js");
|
|
1173
|
+
const result = await handleZendeskConnect(Buffer.concat(chunks).toString("utf-8"));
|
|
1174
|
+
res.writeHead(result.status, {
|
|
1175
|
+
"Content-Type": result.contentType ?? "application/json",
|
|
1176
|
+
});
|
|
1177
|
+
res.end(result.body);
|
|
1178
|
+
})();
|
|
1179
|
+
});
|
|
1180
|
+
return;
|
|
1181
|
+
}
|
|
1182
|
+
if (parsedUrl.pathname === "/connections/zendesk/test" &&
|
|
1183
|
+
req.method === "POST") {
|
|
1184
|
+
void (async () => {
|
|
1185
|
+
const { handleZendeskTest } = await import("./connectors/zendesk.js");
|
|
1186
|
+
const result = await handleZendeskTest();
|
|
1187
|
+
res.writeHead(result.status, {
|
|
1188
|
+
"Content-Type": result.contentType ?? "application/json",
|
|
1189
|
+
});
|
|
1190
|
+
res.end(result.body);
|
|
1191
|
+
})();
|
|
1192
|
+
return;
|
|
1193
|
+
}
|
|
1194
|
+
if (parsedUrl.pathname === "/connections/zendesk" &&
|
|
1195
|
+
req.method === "DELETE") {
|
|
1196
|
+
const { handleZendeskDisconnect } = await import("./connectors/zendesk.js");
|
|
1197
|
+
const result = handleZendeskDisconnect();
|
|
1198
|
+
res.writeHead(result.status, {
|
|
1199
|
+
"Content-Type": result.contentType ?? "application/json",
|
|
1200
|
+
});
|
|
1201
|
+
res.end(result.body);
|
|
1202
|
+
return;
|
|
1203
|
+
}
|
|
1204
|
+
// ── Intercom routes ─────────────────────────────────────────────
|
|
1205
|
+
if (parsedUrl.pathname === "/connections/intercom/connect" &&
|
|
1206
|
+
req.method === "POST") {
|
|
1207
|
+
const chunks = [];
|
|
1208
|
+
req.on("data", (c) => chunks.push(c));
|
|
1209
|
+
req.on("end", () => {
|
|
1210
|
+
void (async () => {
|
|
1211
|
+
const { handleIntercomConnect } = await import("./connectors/intercom.js");
|
|
1212
|
+
const result = await handleIntercomConnect(Buffer.concat(chunks).toString("utf-8"));
|
|
1213
|
+
res.writeHead(result.status, {
|
|
1214
|
+
"Content-Type": result.contentType ?? "application/json",
|
|
1215
|
+
});
|
|
1216
|
+
res.end(result.body);
|
|
1217
|
+
})();
|
|
1218
|
+
});
|
|
1219
|
+
return;
|
|
1220
|
+
}
|
|
1221
|
+
if (parsedUrl.pathname === "/connections/intercom/test" &&
|
|
1222
|
+
req.method === "POST") {
|
|
1223
|
+
void (async () => {
|
|
1224
|
+
const { handleIntercomTest } = await import("./connectors/intercom.js");
|
|
1225
|
+
const result = await handleIntercomTest();
|
|
1226
|
+
res.writeHead(result.status, {
|
|
1227
|
+
"Content-Type": result.contentType ?? "application/json",
|
|
1228
|
+
});
|
|
1229
|
+
res.end(result.body);
|
|
1230
|
+
})();
|
|
1231
|
+
return;
|
|
1232
|
+
}
|
|
1233
|
+
if (parsedUrl.pathname === "/connections/intercom" &&
|
|
1234
|
+
req.method === "DELETE") {
|
|
1235
|
+
const { handleIntercomDisconnect } = await import("./connectors/intercom.js");
|
|
1236
|
+
const result = handleIntercomDisconnect();
|
|
1237
|
+
res.writeHead(result.status, {
|
|
1238
|
+
"Content-Type": result.contentType ?? "application/json",
|
|
1239
|
+
});
|
|
1240
|
+
res.end(result.body);
|
|
1241
|
+
return;
|
|
1242
|
+
}
|
|
1243
|
+
// ── HubSpot routes ─────────────────────────────────────────────
|
|
1244
|
+
if (parsedUrl.pathname === "/connections/hubspot/connect" &&
|
|
1245
|
+
req.method === "POST") {
|
|
1246
|
+
const chunks = [];
|
|
1247
|
+
req.on("data", (c) => chunks.push(c));
|
|
1248
|
+
req.on("end", () => {
|
|
1249
|
+
void (async () => {
|
|
1250
|
+
const { handleHubSpotConnect } = await import("./connectors/hubspot.js");
|
|
1251
|
+
const result = await handleHubSpotConnect(Buffer.concat(chunks).toString("utf-8"));
|
|
1252
|
+
res.writeHead(result.status, {
|
|
1253
|
+
"Content-Type": result.contentType ?? "application/json",
|
|
1254
|
+
});
|
|
1255
|
+
res.end(result.body);
|
|
1256
|
+
})();
|
|
1257
|
+
});
|
|
1258
|
+
return;
|
|
1259
|
+
}
|
|
1260
|
+
if (parsedUrl.pathname === "/connections/hubspot/test" &&
|
|
1261
|
+
req.method === "POST") {
|
|
1262
|
+
const { handleHubSpotTest } = await import("./connectors/hubspot.js");
|
|
1263
|
+
const result = await handleHubSpotTest();
|
|
1264
|
+
res.writeHead(result.status, {
|
|
1265
|
+
"Content-Type": result.contentType ?? "application/json",
|
|
1266
|
+
});
|
|
1267
|
+
res.end(result.body);
|
|
1268
|
+
return;
|
|
1269
|
+
}
|
|
1270
|
+
if (parsedUrl.pathname === "/connections/hubspot" &&
|
|
1271
|
+
req.method === "DELETE") {
|
|
1272
|
+
const { handleHubSpotDisconnect } = await import("./connectors/hubspot.js");
|
|
1273
|
+
const result = handleHubSpotDisconnect();
|
|
1274
|
+
res.writeHead(result.status, {
|
|
1275
|
+
"Content-Type": result.contentType ?? "application/json",
|
|
1276
|
+
});
|
|
1277
|
+
res.end(result.body);
|
|
1278
|
+
return;
|
|
1279
|
+
}
|
|
1280
|
+
// ── Datadog routes ─────────────────────────────────────────────
|
|
1281
|
+
if (parsedUrl.pathname === "/connections/datadog/connect" &&
|
|
1282
|
+
req.method === "POST") {
|
|
1283
|
+
const chunks = [];
|
|
1284
|
+
req.on("data", (c) => chunks.push(c));
|
|
1285
|
+
req.on("end", () => {
|
|
1286
|
+
void (async () => {
|
|
1287
|
+
const { handleDatadogConnect } = await import("./connectors/datadog.js");
|
|
1288
|
+
const result = await handleDatadogConnect(Buffer.concat(chunks).toString("utf-8"));
|
|
1289
|
+
res.writeHead(result.status, {
|
|
1290
|
+
"Content-Type": result.contentType ?? "application/json",
|
|
1291
|
+
});
|
|
1292
|
+
res.end(result.body);
|
|
1293
|
+
})();
|
|
1294
|
+
});
|
|
1295
|
+
return;
|
|
1296
|
+
}
|
|
1297
|
+
if (parsedUrl.pathname === "/connections/datadog/test" &&
|
|
1298
|
+
req.method === "POST") {
|
|
1299
|
+
void (async () => {
|
|
1300
|
+
const { handleDatadogTest } = await import("./connectors/datadog.js");
|
|
1301
|
+
const result = await handleDatadogTest();
|
|
1302
|
+
res.writeHead(result.status, {
|
|
1303
|
+
"Content-Type": result.contentType ?? "application/json",
|
|
1304
|
+
});
|
|
1305
|
+
res.end(result.body);
|
|
1306
|
+
})();
|
|
1307
|
+
return;
|
|
1308
|
+
}
|
|
1309
|
+
if (parsedUrl.pathname === "/connections/datadog" &&
|
|
1310
|
+
req.method === "DELETE") {
|
|
1311
|
+
const { handleDatadogDisconnect } = await import("./connectors/datadog.js");
|
|
1312
|
+
const result = handleDatadogDisconnect();
|
|
1313
|
+
res.writeHead(result.status, {
|
|
1314
|
+
"Content-Type": result.contentType ?? "application/json",
|
|
1315
|
+
});
|
|
1316
|
+
res.end(result.body);
|
|
1317
|
+
return;
|
|
1318
|
+
}
|
|
1319
|
+
// ── Stripe routes ───────────────────────────────────────────────
|
|
1320
|
+
if (parsedUrl.pathname === "/connections/stripe/connect" &&
|
|
1321
|
+
req.method === "POST") {
|
|
1322
|
+
let body = "";
|
|
1323
|
+
req.on("data", (chunk) => {
|
|
1324
|
+
body += chunk.toString();
|
|
1325
|
+
});
|
|
1326
|
+
req.on("end", () => {
|
|
1327
|
+
void (async () => {
|
|
1328
|
+
const { handleStripeConnect } = await import("./connectors/stripe.js");
|
|
1329
|
+
const result = await handleStripeConnect(body);
|
|
1330
|
+
res.writeHead(result.status, {
|
|
1331
|
+
"Content-Type": result.contentType ?? "application/json",
|
|
1332
|
+
});
|
|
1333
|
+
res.end(result.body);
|
|
1334
|
+
})();
|
|
1335
|
+
});
|
|
1336
|
+
return;
|
|
1337
|
+
}
|
|
1338
|
+
if (parsedUrl.pathname === "/connections/stripe/test" &&
|
|
1339
|
+
req.method === "POST") {
|
|
1340
|
+
const { handleStripeTest } = await import("./connectors/stripe.js");
|
|
1341
|
+
const result = await handleStripeTest();
|
|
1342
|
+
res.writeHead(result.status, {
|
|
1343
|
+
"Content-Type": result.contentType ?? "application/json",
|
|
1344
|
+
});
|
|
1345
|
+
res.end(result.body);
|
|
1346
|
+
return;
|
|
1347
|
+
}
|
|
1348
|
+
if (parsedUrl.pathname === "/connections/stripe" &&
|
|
1349
|
+
req.method === "DELETE") {
|
|
1350
|
+
const { handleStripeDisconnect } = await import("./connectors/stripe.js");
|
|
1351
|
+
const result = handleStripeDisconnect();
|
|
1352
|
+
res.writeHead(result.status, {
|
|
1353
|
+
"Content-Type": result.contentType ?? "application/json",
|
|
1354
|
+
});
|
|
1355
|
+
res.end(result.body);
|
|
1356
|
+
return;
|
|
1357
|
+
}
|
|
1358
|
+
// ── Google Calendar routes ──────────────────────────────────────
|
|
1359
|
+
if (parsedUrl.pathname === "/connections/google-calendar/auth" &&
|
|
1360
|
+
req.method === "GET") {
|
|
1361
|
+
void (async () => {
|
|
1362
|
+
const { handleCalendarAuthRedirect } = await import("./connectors/googleCalendar.js");
|
|
1363
|
+
const result = handleCalendarAuthRedirect();
|
|
1364
|
+
if (result.redirect) {
|
|
1365
|
+
res.writeHead(302, { Location: result.redirect });
|
|
1366
|
+
res.end();
|
|
1367
|
+
}
|
|
1368
|
+
else {
|
|
1369
|
+
res.writeHead(result.status, {
|
|
1370
|
+
"Content-Type": result.contentType ?? "application/json",
|
|
1371
|
+
});
|
|
1372
|
+
res.end(result.body);
|
|
1373
|
+
}
|
|
1374
|
+
})();
|
|
1375
|
+
return;
|
|
1376
|
+
}
|
|
1377
|
+
if (parsedUrl.pathname === "/connections/google-calendar/callback" &&
|
|
1378
|
+
req.method === "GET") {
|
|
1379
|
+
void (async () => {
|
|
1380
|
+
const { handleCalendarCallback } = await import("./connectors/googleCalendar.js");
|
|
1381
|
+
const code = parsedUrl.searchParams.get("code");
|
|
1382
|
+
const state = parsedUrl.searchParams.get("state");
|
|
1383
|
+
const error = parsedUrl.searchParams.get("error");
|
|
1384
|
+
const result = await handleCalendarCallback(code, state, error);
|
|
1385
|
+
res.writeHead(result.status, {
|
|
1386
|
+
"Content-Type": result.contentType ?? "application/json",
|
|
1387
|
+
});
|
|
1388
|
+
res.end(result.body);
|
|
1389
|
+
})();
|
|
1390
|
+
return;
|
|
1391
|
+
}
|
|
1392
|
+
if (parsedUrl.pathname === "/connections/google-calendar/test" &&
|
|
1393
|
+
req.method === "POST") {
|
|
1394
|
+
void (async () => {
|
|
1395
|
+
const { handleCalendarTest } = await import("./connectors/googleCalendar.js");
|
|
1396
|
+
const result = await handleCalendarTest();
|
|
1397
|
+
res.writeHead(result.status, {
|
|
1398
|
+
"Content-Type": result.contentType ?? "application/json",
|
|
1399
|
+
});
|
|
1400
|
+
res.end(result.body);
|
|
1401
|
+
})();
|
|
1402
|
+
return;
|
|
1403
|
+
}
|
|
1404
|
+
if (parsedUrl.pathname === "/connections/google-calendar" &&
|
|
1405
|
+
req.method === "DELETE") {
|
|
1406
|
+
void (async () => {
|
|
1407
|
+
const { handleCalendarDisconnect } = await import("./connectors/googleCalendar.js");
|
|
1408
|
+
const result = await handleCalendarDisconnect();
|
|
1409
|
+
res.writeHead(result.status, {
|
|
1410
|
+
"Content-Type": result.contentType ?? "application/json",
|
|
1411
|
+
});
|
|
1412
|
+
res.end(result.body);
|
|
1413
|
+
})();
|
|
1414
|
+
return;
|
|
1415
|
+
}
|
|
1416
|
+
// ── Inbox routes ────────────────────────────────────────────────────
|
|
1417
|
+
if (parsedUrl.pathname === "/inbox" && req.method === "GET") {
|
|
1418
|
+
void (async () => {
|
|
1419
|
+
try {
|
|
1420
|
+
const { readdir, readFile, stat } = await import("node:fs/promises");
|
|
1421
|
+
const { existsSync } = await import("node:fs");
|
|
1422
|
+
const inboxDir = path.join(os.homedir(), ".patchwork", "inbox");
|
|
1423
|
+
if (!existsSync(inboxDir)) {
|
|
1424
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
1425
|
+
res.end(JSON.stringify({ items: [] }));
|
|
1426
|
+
return;
|
|
1427
|
+
}
|
|
1428
|
+
const files = (await readdir(inboxDir)).filter((f) => f.endsWith(".md"));
|
|
1429
|
+
const items = await Promise.all(files.map(async (name) => {
|
|
1430
|
+
const filePath = path.join(inboxDir, name);
|
|
1431
|
+
const [content, stats] = await Promise.all([
|
|
1432
|
+
readFile(filePath, "utf8"),
|
|
1433
|
+
stat(filePath),
|
|
1434
|
+
]);
|
|
1435
|
+
const stripped = content
|
|
1436
|
+
.split("\n")
|
|
1437
|
+
.filter((l) => !l.startsWith("#"))
|
|
1438
|
+
.join("\n")
|
|
1439
|
+
.trim();
|
|
1440
|
+
return {
|
|
1441
|
+
name,
|
|
1442
|
+
path: filePath,
|
|
858
1443
|
modifiedAt: stats.mtime.toISOString(),
|
|
859
1444
|
preview: stripped.slice(0, 200),
|
|
860
1445
|
};
|
|
@@ -914,6 +1499,47 @@ export class Server extends EventEmitter {
|
|
|
914
1499
|
return;
|
|
915
1500
|
}
|
|
916
1501
|
// ── End inbox routes ─────────────────────────────────────────────────
|
|
1502
|
+
const recipeNameRunMatch = req.method === "POST"
|
|
1503
|
+
? /^\/recipes\/([^/]+)\/run$/.exec(parsedUrl.pathname)
|
|
1504
|
+
: null;
|
|
1505
|
+
if (recipeNameRunMatch) {
|
|
1506
|
+
const nameFromPath = decodeURIComponent(recipeNameRunMatch[1] ?? "");
|
|
1507
|
+
const chunks = [];
|
|
1508
|
+
req.on("data", (c) => chunks.push(c));
|
|
1509
|
+
req.on("end", () => {
|
|
1510
|
+
void (async () => {
|
|
1511
|
+
try {
|
|
1512
|
+
const body = Buffer.concat(chunks).toString("utf-8");
|
|
1513
|
+
const parsed = body
|
|
1514
|
+
? JSON.parse(body)
|
|
1515
|
+
: {};
|
|
1516
|
+
const vars = parsed.vars &&
|
|
1517
|
+
typeof parsed.vars === "object" &&
|
|
1518
|
+
!Array.isArray(parsed.vars)
|
|
1519
|
+
? parsed.vars
|
|
1520
|
+
: undefined;
|
|
1521
|
+
if (!this.runRecipeFn) {
|
|
1522
|
+
res.writeHead(503, { "Content-Type": "application/json" });
|
|
1523
|
+
res.end(JSON.stringify({
|
|
1524
|
+
ok: false,
|
|
1525
|
+
error: "Recipe execution unavailable — requires --claude-driver subprocess",
|
|
1526
|
+
}));
|
|
1527
|
+
return;
|
|
1528
|
+
}
|
|
1529
|
+
const result = await this.runRecipeFn(nameFromPath, vars);
|
|
1530
|
+
res.writeHead(result.ok ? 200 : 400, {
|
|
1531
|
+
"Content-Type": "application/json",
|
|
1532
|
+
});
|
|
1533
|
+
res.end(JSON.stringify(result));
|
|
1534
|
+
}
|
|
1535
|
+
catch {
|
|
1536
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
1537
|
+
res.end(JSON.stringify({ ok: false, error: "Invalid JSON body" }));
|
|
1538
|
+
}
|
|
1539
|
+
})();
|
|
1540
|
+
});
|
|
1541
|
+
return;
|
|
1542
|
+
}
|
|
917
1543
|
if (parsedUrl.pathname === "/recipes/run" && req.method === "POST") {
|
|
918
1544
|
const chunks = [];
|
|
919
1545
|
req.on("data", (c) => chunks.push(c));
|
|
@@ -923,6 +1549,11 @@ export class Server extends EventEmitter {
|
|
|
923
1549
|
const body = Buffer.concat(chunks).toString("utf-8");
|
|
924
1550
|
const parsed = JSON.parse(body || "{}");
|
|
925
1551
|
const name = parsed.name;
|
|
1552
|
+
const vars = parsed.vars &&
|
|
1553
|
+
typeof parsed.vars === "object" &&
|
|
1554
|
+
!Array.isArray(parsed.vars)
|
|
1555
|
+
? parsed.vars
|
|
1556
|
+
: undefined;
|
|
926
1557
|
if (typeof name !== "string" || !name) {
|
|
927
1558
|
res.writeHead(400, { "Content-Type": "application/json" });
|
|
928
1559
|
res.end(JSON.stringify({ ok: false, error: "name required" }));
|
|
@@ -936,7 +1567,7 @@ export class Server extends EventEmitter {
|
|
|
936
1567
|
}));
|
|
937
1568
|
return;
|
|
938
1569
|
}
|
|
939
|
-
const result = await this.runRecipeFn(name);
|
|
1570
|
+
const result = await this.runRecipeFn(name, vars);
|
|
940
1571
|
res.writeHead(result.ok ? 200 : 400, {
|
|
941
1572
|
"Content-Type": "application/json",
|
|
942
1573
|
});
|
|
@@ -950,6 +1581,22 @@ export class Server extends EventEmitter {
|
|
|
950
1581
|
});
|
|
951
1582
|
return;
|
|
952
1583
|
}
|
|
1584
|
+
if (parsedUrl.pathname === "/activation-metrics" &&
|
|
1585
|
+
req.method === "GET") {
|
|
1586
|
+
try {
|
|
1587
|
+
const metrics = loadActivationMetrics();
|
|
1588
|
+
const summary = computeActivationSummary(metrics);
|
|
1589
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
1590
|
+
res.end(JSON.stringify({ metrics, summary }));
|
|
1591
|
+
}
|
|
1592
|
+
catch (err) {
|
|
1593
|
+
res.writeHead(500, { "Content-Type": "application/json" });
|
|
1594
|
+
res.end(JSON.stringify({
|
|
1595
|
+
error: err instanceof Error ? err.message : String(err),
|
|
1596
|
+
}));
|
|
1597
|
+
}
|
|
1598
|
+
return;
|
|
1599
|
+
}
|
|
953
1600
|
if (parsedUrl.pathname === "/runs" && req.method === "GET") {
|
|
954
1601
|
try {
|
|
955
1602
|
const sp = parsedUrl.searchParams;
|
|
@@ -978,6 +1625,62 @@ export class Server extends EventEmitter {
|
|
|
978
1625
|
}
|
|
979
1626
|
return;
|
|
980
1627
|
}
|
|
1628
|
+
// GET /runs/:seq — single run detail (includes stepResults if present)
|
|
1629
|
+
const runDetailMatch = req.method === "GET"
|
|
1630
|
+
? /^\/runs\/(\d+)$/.exec(parsedUrl.pathname)
|
|
1631
|
+
: null;
|
|
1632
|
+
if (runDetailMatch?.[1]) {
|
|
1633
|
+
const seq = Number.parseInt(runDetailMatch[1], 10);
|
|
1634
|
+
try {
|
|
1635
|
+
const run = this.runDetailFn?.(seq) ?? null;
|
|
1636
|
+
if (!run) {
|
|
1637
|
+
res.writeHead(404, { "Content-Type": "application/json" });
|
|
1638
|
+
res.end(JSON.stringify({ error: "not_found" }));
|
|
1639
|
+
}
|
|
1640
|
+
else {
|
|
1641
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
1642
|
+
res.end(JSON.stringify({ run }));
|
|
1643
|
+
}
|
|
1644
|
+
}
|
|
1645
|
+
catch (err) {
|
|
1646
|
+
res.writeHead(500, { "Content-Type": "application/json" });
|
|
1647
|
+
res.end(JSON.stringify({
|
|
1648
|
+
error: err instanceof Error ? err.message : String(err),
|
|
1649
|
+
}));
|
|
1650
|
+
}
|
|
1651
|
+
return;
|
|
1652
|
+
}
|
|
1653
|
+
// GET /runs/:seq/plan — dry-run plan for the recipe that produced this run
|
|
1654
|
+
const runPlanMatch = req.method === "GET"
|
|
1655
|
+
? /^\/runs\/(\d+)\/plan$/.exec(parsedUrl.pathname)
|
|
1656
|
+
: null;
|
|
1657
|
+
if (runPlanMatch?.[1]) {
|
|
1658
|
+
const seq = Number.parseInt(runPlanMatch[1], 10);
|
|
1659
|
+
try {
|
|
1660
|
+
const run = this.runDetailFn?.(seq) ?? null;
|
|
1661
|
+
if (!run) {
|
|
1662
|
+
res.writeHead(404, { "Content-Type": "application/json" });
|
|
1663
|
+
res.end(JSON.stringify({ error: "run_not_found" }));
|
|
1664
|
+
return;
|
|
1665
|
+
}
|
|
1666
|
+
if (!this.runPlanFn) {
|
|
1667
|
+
res.writeHead(503, { "Content-Type": "application/json" });
|
|
1668
|
+
res.end(JSON.stringify({ error: "plan_unavailable" }));
|
|
1669
|
+
return;
|
|
1670
|
+
}
|
|
1671
|
+
const recipeName = run.recipeName;
|
|
1672
|
+
const plan = await this.runPlanFn(recipeName);
|
|
1673
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
1674
|
+
res.end(JSON.stringify({ plan }));
|
|
1675
|
+
}
|
|
1676
|
+
catch (err) {
|
|
1677
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
1678
|
+
const status = msg.includes("not found") || msg.includes("ENOENT") ? 404 : 500;
|
|
1679
|
+
res.writeHead(status, { "Content-Type": "application/json" });
|
|
1680
|
+
res.end(JSON.stringify({ error: msg }));
|
|
1681
|
+
}
|
|
1682
|
+
return;
|
|
1683
|
+
}
|
|
981
1684
|
if (req.url === "/recipes" && req.method === "POST") {
|
|
982
1685
|
const chunks = [];
|
|
983
1686
|
req.on("data", (c) => chunks.push(c));
|
|
@@ -1011,6 +1714,94 @@ export class Server extends EventEmitter {
|
|
|
1011
1714
|
});
|
|
1012
1715
|
return;
|
|
1013
1716
|
}
|
|
1717
|
+
const recipePatchMatch = /^\/recipes\/([^/]+)$/.exec(parsedUrl.pathname);
|
|
1718
|
+
if (recipePatchMatch && req.method === "PATCH") {
|
|
1719
|
+
const name = decodeURIComponent(recipePatchMatch[1]);
|
|
1720
|
+
const chunks = [];
|
|
1721
|
+
req.on("data", (c) => chunks.push(c));
|
|
1722
|
+
req.on("end", () => {
|
|
1723
|
+
try {
|
|
1724
|
+
const body = JSON.parse(Buffer.concat(chunks).toString("utf-8"));
|
|
1725
|
+
if (typeof body.enabled !== "boolean") {
|
|
1726
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
1727
|
+
res.end(JSON.stringify({
|
|
1728
|
+
ok: false,
|
|
1729
|
+
error: "enabled (boolean) required",
|
|
1730
|
+
}));
|
|
1731
|
+
return;
|
|
1732
|
+
}
|
|
1733
|
+
if (!this.setRecipeEnabledFn) {
|
|
1734
|
+
res.writeHead(503, { "Content-Type": "application/json" });
|
|
1735
|
+
res.end(JSON.stringify({ ok: false, error: "Not available" }));
|
|
1736
|
+
return;
|
|
1737
|
+
}
|
|
1738
|
+
const result = this.setRecipeEnabledFn(name, body.enabled);
|
|
1739
|
+
res.writeHead(result.ok ? 200 : 400, {
|
|
1740
|
+
"Content-Type": "application/json",
|
|
1741
|
+
});
|
|
1742
|
+
res.end(JSON.stringify(result));
|
|
1743
|
+
}
|
|
1744
|
+
catch {
|
|
1745
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
1746
|
+
res.end(JSON.stringify({ ok: false, error: "Invalid JSON" }));
|
|
1747
|
+
}
|
|
1748
|
+
});
|
|
1749
|
+
return;
|
|
1750
|
+
}
|
|
1751
|
+
const recipeContentMatch = /^\/recipes\/([^/]+)$/.exec(parsedUrl.pathname);
|
|
1752
|
+
if (recipeContentMatch && req.method === "GET") {
|
|
1753
|
+
const name = decodeURIComponent(recipeContentMatch[1]);
|
|
1754
|
+
if (!this.loadRecipeContentFn) {
|
|
1755
|
+
res.writeHead(503, { "Content-Type": "application/json" });
|
|
1756
|
+
res.end(JSON.stringify({ ok: false, error: "Recipe content unavailable" }));
|
|
1757
|
+
return;
|
|
1758
|
+
}
|
|
1759
|
+
const result = this.loadRecipeContentFn(name);
|
|
1760
|
+
if (!result) {
|
|
1761
|
+
res.writeHead(404, { "Content-Type": "application/json" });
|
|
1762
|
+
res.end(JSON.stringify({ ok: false, error: "Recipe not found" }));
|
|
1763
|
+
return;
|
|
1764
|
+
}
|
|
1765
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
1766
|
+
res.end(JSON.stringify(result));
|
|
1767
|
+
return;
|
|
1768
|
+
}
|
|
1769
|
+
if (recipeContentMatch && req.method === "PUT") {
|
|
1770
|
+
const name = decodeURIComponent(recipeContentMatch[1]);
|
|
1771
|
+
const chunks = [];
|
|
1772
|
+
req.on("data", (c) => chunks.push(c));
|
|
1773
|
+
req.on("end", () => {
|
|
1774
|
+
try {
|
|
1775
|
+
const body = JSON.parse(Buffer.concat(chunks).toString("utf-8"));
|
|
1776
|
+
if (typeof body.content !== "string") {
|
|
1777
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
1778
|
+
res.end(JSON.stringify({
|
|
1779
|
+
ok: false,
|
|
1780
|
+
error: "content (string) required",
|
|
1781
|
+
}));
|
|
1782
|
+
return;
|
|
1783
|
+
}
|
|
1784
|
+
if (!this.saveRecipeContentFn) {
|
|
1785
|
+
res.writeHead(503, { "Content-Type": "application/json" });
|
|
1786
|
+
res.end(JSON.stringify({
|
|
1787
|
+
ok: false,
|
|
1788
|
+
error: "Recipe content saving unavailable",
|
|
1789
|
+
}));
|
|
1790
|
+
return;
|
|
1791
|
+
}
|
|
1792
|
+
const result = this.saveRecipeContentFn(name, body.content);
|
|
1793
|
+
res.writeHead(result.ok ? 200 : 400, {
|
|
1794
|
+
"Content-Type": "application/json",
|
|
1795
|
+
});
|
|
1796
|
+
res.end(JSON.stringify(result));
|
|
1797
|
+
}
|
|
1798
|
+
catch {
|
|
1799
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
1800
|
+
res.end(JSON.stringify({ ok: false, error: "Invalid JSON body" }));
|
|
1801
|
+
}
|
|
1802
|
+
});
|
|
1803
|
+
return;
|
|
1804
|
+
}
|
|
1014
1805
|
if (req.url === "/recipes" && req.method === "GET") {
|
|
1015
1806
|
try {
|
|
1016
1807
|
const data = this.recipesFn?.() ?? { recipesDir: null, recipes: [] };
|
|
@@ -1071,8 +1862,11 @@ export class Server extends EventEmitter {
|
|
|
1071
1862
|
req.on("end", () => {
|
|
1072
1863
|
try {
|
|
1073
1864
|
const body = JSON.parse(Buffer.concat(chunks).toString("utf-8"));
|
|
1074
|
-
const
|
|
1075
|
-
|
|
1865
|
+
const hasWebhookUpdate = body.webhookUrl !== undefined;
|
|
1866
|
+
const raw = hasWebhookUpdate
|
|
1867
|
+
? (body.webhookUrl?.trim() ?? "")
|
|
1868
|
+
: undefined;
|
|
1869
|
+
if (raw !== undefined && raw !== "" && !/^https:\/\/.+/.test(raw)) {
|
|
1076
1870
|
res.writeHead(400, { "Content-Type": "application/json" });
|
|
1077
1871
|
res.end(JSON.stringify({ error: "webhookUrl must be HTTPS" }));
|
|
1078
1872
|
return;
|
|
@@ -1094,16 +1888,94 @@ export class Server extends EventEmitter {
|
|
|
1094
1888
|
port: cfg.dashboard?.port ?? 3000,
|
|
1095
1889
|
requireApproval: cfg.dashboard?.requireApproval ?? ["high"],
|
|
1096
1890
|
pushNotifications: cfg.dashboard?.pushNotifications ?? false,
|
|
1097
|
-
webhookUrl:
|
|
1891
|
+
webhookUrl: hasWebhookUpdate
|
|
1892
|
+
? raw || undefined
|
|
1893
|
+
: cfg.dashboard?.webhookUrl,
|
|
1098
1894
|
};
|
|
1099
1895
|
if (gateRaw !== undefined) {
|
|
1100
1896
|
cfg.approvalGate = gateRaw;
|
|
1101
1897
|
this.approvalGate = gateRaw;
|
|
1102
1898
|
}
|
|
1899
|
+
const driverRaw = body.driver;
|
|
1900
|
+
if (driverRaw !== undefined) {
|
|
1901
|
+
const validDrivers = [
|
|
1902
|
+
"subprocess",
|
|
1903
|
+
"api",
|
|
1904
|
+
"openai",
|
|
1905
|
+
"grok",
|
|
1906
|
+
"gemini",
|
|
1907
|
+
"none",
|
|
1908
|
+
];
|
|
1909
|
+
if (!validDrivers.includes(driverRaw)) {
|
|
1910
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
1911
|
+
res.end(JSON.stringify({
|
|
1912
|
+
error: `driver must be one of: ${validDrivers.join(", ")}`,
|
|
1913
|
+
}));
|
|
1914
|
+
return;
|
|
1915
|
+
}
|
|
1916
|
+
const driver = driverRaw;
|
|
1917
|
+
cfg.driver = driver;
|
|
1918
|
+
saveBridgeConfigDriver(driver, this.bridgeConfigPath);
|
|
1919
|
+
}
|
|
1920
|
+
if (body.model !== undefined) {
|
|
1921
|
+
const validModels = [
|
|
1922
|
+
"claude",
|
|
1923
|
+
"openai",
|
|
1924
|
+
"gemini",
|
|
1925
|
+
"grok",
|
|
1926
|
+
"local",
|
|
1927
|
+
];
|
|
1928
|
+
if (!validModels.includes(body.model)) {
|
|
1929
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
1930
|
+
res.end(JSON.stringify({
|
|
1931
|
+
error: `model must be one of: ${validModels.join(", ")}`,
|
|
1932
|
+
}));
|
|
1933
|
+
return;
|
|
1934
|
+
}
|
|
1935
|
+
cfg.model = body.model;
|
|
1936
|
+
if (body.model === "local") {
|
|
1937
|
+
if (body.localEndpoint !== undefined)
|
|
1938
|
+
cfg.localEndpoint = body.localEndpoint.trim() || undefined;
|
|
1939
|
+
if (body.localModel !== undefined)
|
|
1940
|
+
cfg.localModel = body.localModel.trim() || undefined;
|
|
1941
|
+
}
|
|
1942
|
+
}
|
|
1943
|
+
if (body.apiKey) {
|
|
1944
|
+
const { provider, key } = body.apiKey;
|
|
1945
|
+
const validProviders = ["anthropic", "openai", "google", "xai"];
|
|
1946
|
+
if (!validProviders.includes(provider) ||
|
|
1947
|
+
typeof key !== "string") {
|
|
1948
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
1949
|
+
res.end(JSON.stringify({ error: "Invalid apiKey provider or key" }));
|
|
1950
|
+
return;
|
|
1951
|
+
}
|
|
1952
|
+
cfg.apiKeys = { ...cfg.apiKeys, [provider]: key || undefined };
|
|
1953
|
+
}
|
|
1103
1954
|
savePatchworkConfig(cfg, configPath);
|
|
1104
|
-
|
|
1955
|
+
if (hasWebhookUpdate) {
|
|
1956
|
+
this.approvalWebhookUrl = raw || undefined;
|
|
1957
|
+
}
|
|
1958
|
+
if (body.pushServiceUrl !== undefined) {
|
|
1959
|
+
const pushUrl = body.pushServiceUrl.trim();
|
|
1960
|
+
if (pushUrl && !pushUrl.startsWith("https://")) {
|
|
1961
|
+
res.writeHead(400, { "Content-Type": "application/json" });
|
|
1962
|
+
res.end(JSON.stringify({ error: "pushServiceUrl must be HTTPS" }));
|
|
1963
|
+
return;
|
|
1964
|
+
}
|
|
1965
|
+
this.pushServiceUrl = pushUrl || undefined;
|
|
1966
|
+
}
|
|
1967
|
+
if (body.pushServiceToken !== undefined) {
|
|
1968
|
+
this.pushServiceToken = body.pushServiceToken.trim() || undefined;
|
|
1969
|
+
}
|
|
1970
|
+
if (body.pushServiceBaseUrl !== undefined) {
|
|
1971
|
+
this.pushServiceBaseUrl =
|
|
1972
|
+
body.pushServiceBaseUrl.trim() || undefined;
|
|
1973
|
+
}
|
|
1974
|
+
const restartRequired = driverRaw !== undefined ||
|
|
1975
|
+
body.apiKey !== undefined ||
|
|
1976
|
+
body.model !== undefined;
|
|
1105
1977
|
res.writeHead(200, { "Content-Type": "application/json" });
|
|
1106
|
-
res.end(JSON.stringify({ ok: true }));
|
|
1978
|
+
res.end(JSON.stringify({ ok: true, restartRequired }));
|
|
1107
1979
|
}
|
|
1108
1980
|
catch (err) {
|
|
1109
1981
|
res.writeHead(400, { "Content-Type": "application/json" });
|
|
@@ -1167,6 +2039,11 @@ export class Server extends EventEmitter {
|
|
|
1167
2039
|
}
|
|
1168
2040
|
return;
|
|
1169
2041
|
}
|
|
2042
|
+
// SSE stream for live approval queue updates.
|
|
2043
|
+
if (parsedUrl.pathname === "/approvals/stream" && req.method === "GET") {
|
|
2044
|
+
handleApprovalsStream(res, { queue: getApprovalQueue() }, parsedUrl.searchParams.get("session"));
|
|
2045
|
+
return;
|
|
2046
|
+
}
|
|
1170
2047
|
// Patchwork approval surface — PreToolUse hook + dashboard approve/reject.
|
|
1171
2048
|
// Bearer auth already checked above.
|
|
1172
2049
|
if (parsedUrl.pathname === "/approvals" ||
|
|
@@ -1192,6 +2069,7 @@ export class Server extends EventEmitter {
|
|
|
1192
2069
|
path: parsedUrl.pathname,
|
|
1193
2070
|
body: parsedBody,
|
|
1194
2071
|
query: parsedUrl.searchParams,
|
|
2072
|
+
approvalToken: req.headers["x-approval-token"],
|
|
1195
2073
|
}, {
|
|
1196
2074
|
queue: getApprovalQueue(),
|
|
1197
2075
|
workspace: process.cwd(),
|
|
@@ -1199,6 +2077,9 @@ export class Server extends EventEmitter {
|
|
|
1199
2077
|
onDecision: this.onApprovalDecision,
|
|
1200
2078
|
webhookUrl: this.approvalWebhookUrl,
|
|
1201
2079
|
approvalGate: this.approvalGate,
|
|
2080
|
+
pushServiceUrl: this.pushServiceUrl,
|
|
2081
|
+
pushServiceToken: this.pushServiceToken,
|
|
2082
|
+
pushServiceBaseUrl: this.pushServiceBaseUrl,
|
|
1202
2083
|
});
|
|
1203
2084
|
res.writeHead(result.status, {
|
|
1204
2085
|
"Content-Type": "application/json",
|
|
@@ -1397,6 +2278,8 @@ export class Server extends EventEmitter {
|
|
|
1397
2278
|
ws.on("error", (err) => {
|
|
1398
2279
|
this.logger.error(`WebSocket client error: ${err.message}`);
|
|
1399
2280
|
});
|
|
2281
|
+
ws.remoteAddr =
|
|
2282
|
+
req.socket.remoteAddress;
|
|
1400
2283
|
this.emit("connection", ws);
|
|
1401
2284
|
});
|
|
1402
2285
|
}
|