patchwork-os 0.2.0-alpha.3 → 0.2.0-alpha.30
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 +7 -0
- package/dist/bridge.js +225 -35
- 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/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/bridge.js
CHANGED
|
@@ -3,6 +3,7 @@ import fs from "node:fs";
|
|
|
3
3
|
import os from "node:os";
|
|
4
4
|
import path from "node:path";
|
|
5
5
|
import { WebSocket } from "ws";
|
|
6
|
+
import { recordRecipeRun } from "./activationMetrics.js";
|
|
6
7
|
import { ActivityLog } from "./activityLog.js";
|
|
7
8
|
import { buildSummary } from "./analyticsAggregator.js";
|
|
8
9
|
import { getAnalyticsPref } from "./analyticsPrefs.js";
|
|
@@ -11,21 +12,23 @@ import { getApprovalQueue } from "./approvalQueue.js";
|
|
|
11
12
|
import { AutomationHooks, loadPolicy } from "./automation.js";
|
|
12
13
|
import { loadOrCreateBridgeToken } from "./bridgeToken.js";
|
|
13
14
|
import { repairBridgeToolsRulesIfStale } from "./bridgeToolsRules.js";
|
|
14
|
-
import { createDriver } from "./claudeDriver.js";
|
|
15
15
|
import { ClaudeOrchestrator } from "./claudeOrchestrator.js";
|
|
16
16
|
import { CommitIssueLinkLog } from "./commitIssueLinkLog.js";
|
|
17
17
|
import { DecisionTraceLog } from "./decisionTraceLog.js";
|
|
18
|
+
import { createDriver } from "./drivers/index.js";
|
|
18
19
|
import { ExtensionClient } from "./extensionClient.js";
|
|
19
20
|
import { FileLock } from "./fileLock.js";
|
|
20
21
|
import { buildEnforcementReminder } from "./instructionsUtils.js";
|
|
21
22
|
import { LockFileManager } from "./lockfile.js";
|
|
22
23
|
import { Logger } from "./logger.js";
|
|
23
24
|
import { OAuthServerImpl } from "./oauth.js";
|
|
25
|
+
import { loadConfig as loadPatchworkConfig, saveConfig as savePatchworkConfig, } from "./patchworkConfig.js";
|
|
24
26
|
import { loadPlugins, loadPluginsFull } from "./pluginLoader.js";
|
|
25
27
|
import { PluginWatcher } from "./pluginWatcher.js";
|
|
26
28
|
import { probeAll } from "./probe.js";
|
|
29
|
+
import { RecipeOrchestrator } from "./recipes/RecipeOrchestrator.js";
|
|
27
30
|
import { RecipeScheduler } from "./recipes/scheduler.js";
|
|
28
|
-
import { findWebhookRecipe, listInstalledRecipes, loadRecipePrompt, renderWebhookPrompt, saveRecipe, } from "./recipesHttp.js";
|
|
31
|
+
import { findWebhookRecipe, findYamlRecipePath, listInstalledRecipes, loadRecipeContent, loadRecipePrompt, renderWebhookPrompt, saveRecipe, saveRecipeContent, } from "./recipesHttp.js";
|
|
29
32
|
import { classifyTool } from "./riskTier.js";
|
|
30
33
|
import { RecipeRunLog } from "./runLog.js";
|
|
31
34
|
import { Server } from "./server.js";
|
|
@@ -114,6 +117,7 @@ export class Bridge {
|
|
|
114
117
|
automationHooks = undefined;
|
|
115
118
|
recipeScheduler = null;
|
|
116
119
|
recipeRunLog = null;
|
|
120
|
+
recipeOrchestrator = null;
|
|
117
121
|
commitIssueLinkLog = null;
|
|
118
122
|
decisionTraceLog = null;
|
|
119
123
|
/** Pre-computed digest of recent decisions, refreshed on each session connect. */
|
|
@@ -136,6 +140,7 @@ export class Bridge {
|
|
|
136
140
|
const configDir = process.env.CLAUDE_CONFIG_DIR ?? path.join(os.homedir(), ".claude");
|
|
137
141
|
this.authToken = config.fixedToken ?? loadOrCreateBridgeToken(configDir);
|
|
138
142
|
this.server = new Server(this.authToken, this.logger, config.corsOrigins);
|
|
143
|
+
this.server.bridgeConfigPath = config.configFilePath ?? undefined;
|
|
139
144
|
if (config.issuerUrl) {
|
|
140
145
|
this.oauthServer = new OAuthServerImpl(this.authToken, config.issuerUrl, {
|
|
141
146
|
configDir,
|
|
@@ -239,6 +244,8 @@ export class Bridge {
|
|
|
239
244
|
transport.sessionId = sessionId;
|
|
240
245
|
transport.setActivityLog(this.activityLog);
|
|
241
246
|
transport.setToolRateLimit(this.config.toolRateLimit);
|
|
247
|
+
if (this.config.lazyTools)
|
|
248
|
+
transport.setLazyTools(true);
|
|
242
249
|
if (this.server.approvalGate !== "off") {
|
|
243
250
|
this.logger.info(`[patchwork] approval gate active: ${this.server.approvalGate} tier(s) require dashboard approval`);
|
|
244
251
|
transport.setApprovalGate(async ({ toolName, params, sessionId }) => {
|
|
@@ -309,6 +316,7 @@ export class Bridge {
|
|
|
309
316
|
graceTimer: null,
|
|
310
317
|
connectedAt: Date.now(),
|
|
311
318
|
wsAlive: true,
|
|
319
|
+
remoteAddr: ws.remoteAddr,
|
|
312
320
|
};
|
|
313
321
|
ws.on("pong", () => {
|
|
314
322
|
session.wsAlive = true;
|
|
@@ -702,8 +710,19 @@ export class Bridge {
|
|
|
702
710
|
this.logger.info(`Available linters: ${probeList(["tsc", "eslint", "pyright", "ruff", "cargo", "go", "biome"])}`);
|
|
703
711
|
this.logger.info(`Available test runners: ${probeList(["vitest", "jest", "pytest", "cargo", "go"])}`);
|
|
704
712
|
// 2. Initialize Claude driver and orchestrator (if configured)
|
|
705
|
-
if (this.config.
|
|
706
|
-
const driver = createDriver(this.config.
|
|
713
|
+
if (this.config.driver !== "none") {
|
|
714
|
+
const driver = createDriver(this.config.driver, {
|
|
715
|
+
binary: this.config.claudeBinary,
|
|
716
|
+
antBinary: this.config.antBinary,
|
|
717
|
+
bridgeMcp: this.config.driver === "gemini"
|
|
718
|
+
? () => this.port > 0
|
|
719
|
+
? {
|
|
720
|
+
url: `http://127.0.0.1:${this.port}/mcp`,
|
|
721
|
+
authToken: this.authToken,
|
|
722
|
+
}
|
|
723
|
+
: undefined
|
|
724
|
+
: undefined,
|
|
725
|
+
}, (msg) => this.logger.info(msg));
|
|
707
726
|
// Patchwork: enrichment link log is useful regardless of orchestrator.
|
|
708
727
|
const patchworkDir = path.join(os.homedir(), ".patchwork");
|
|
709
728
|
this.commitIssueLinkLog = new CommitIssueLinkLog({
|
|
@@ -763,17 +782,25 @@ export class Bridge {
|
|
|
763
782
|
},
|
|
764
783
|
});
|
|
765
784
|
this.logger.info(`[bridge] Claude driver: ${driver.name}`);
|
|
785
|
+
// Recipe orchestrator — owns in-flight dedup across all entry paths.
|
|
786
|
+
this.recipeOrchestrator = new RecipeOrchestrator({
|
|
787
|
+
workdir: this.config.workspace,
|
|
788
|
+
});
|
|
766
789
|
// Patchwork: start cron-trigger scheduler once the orchestrator exists.
|
|
767
790
|
const recipesDir = path.join(os.homedir(), ".patchwork", "recipes");
|
|
768
791
|
this.recipeScheduler = new RecipeScheduler({
|
|
769
792
|
recipesDir,
|
|
770
|
-
enqueue: (opts) => this.orchestrator
|
|
793
|
+
enqueue: (opts) => this.orchestrator?.enqueue(opts) ?? "",
|
|
794
|
+
runYaml: async (name) => {
|
|
795
|
+
const result = await this.server.runRecipeFn?.(name);
|
|
796
|
+
if (result && !result.ok) {
|
|
797
|
+
throw new Error(result.error ?? "unknown error");
|
|
798
|
+
}
|
|
799
|
+
},
|
|
771
800
|
logger: this.logger,
|
|
772
801
|
});
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
this.logger.info(`[patchwork] scheduled ${scheduled.length} cron recipe${scheduled.length === 1 ? "" : "s"}`);
|
|
776
|
-
}
|
|
802
|
+
// scheduler.start() deferred to after this.port is set (see below)
|
|
803
|
+
// so bridgeMcp callback has a valid port when first cron fires.
|
|
777
804
|
}
|
|
778
805
|
}
|
|
779
806
|
if (this.config.automationEnabled) {
|
|
@@ -973,6 +1000,7 @@ export class Bridge {
|
|
|
973
1000
|
};
|
|
974
1001
|
};
|
|
975
1002
|
this.server.streamFn = (listener) => this.activityLog.subscribe(listener);
|
|
1003
|
+
this.server.cancelTaskFn = (id) => this.orchestrator?.cancel(id, "user") ?? false;
|
|
976
1004
|
this.server.tasksFn = () => ({
|
|
977
1005
|
tasks: (this.orchestrator?.list() ?? []).map((t) => ({
|
|
978
1006
|
taskId: t.id,
|
|
@@ -1003,10 +1031,43 @@ export class Bridge {
|
|
|
1003
1031
|
const recipesDir = path.join(os.homedir(), ".patchwork", "recipes");
|
|
1004
1032
|
return listInstalledRecipes(recipesDir);
|
|
1005
1033
|
};
|
|
1034
|
+
this.server.loadRecipeContentFn = (name) => {
|
|
1035
|
+
const recipesDir = path.join(os.homedir(), ".patchwork", "recipes");
|
|
1036
|
+
return loadRecipeContent(recipesDir, name);
|
|
1037
|
+
};
|
|
1038
|
+
this.server.saveRecipeContentFn = (name, content) => {
|
|
1039
|
+
const recipesDir = path.join(os.homedir(), ".patchwork", "recipes");
|
|
1040
|
+
return saveRecipeContent(recipesDir, name, content);
|
|
1041
|
+
};
|
|
1006
1042
|
this.server.saveRecipeFn = (draft) => {
|
|
1007
1043
|
const recipesDir = path.join(os.homedir(), ".patchwork", "recipes");
|
|
1008
1044
|
return saveRecipe(recipesDir, draft);
|
|
1009
1045
|
};
|
|
1046
|
+
this.server.setRecipeEnabledFn = (name, enabled) => {
|
|
1047
|
+
try {
|
|
1048
|
+
const cfg = loadPatchworkConfig();
|
|
1049
|
+
const disabled = new Set(cfg.recipes?.disabled ??
|
|
1050
|
+
[]);
|
|
1051
|
+
if (enabled)
|
|
1052
|
+
disabled.delete(name);
|
|
1053
|
+
else
|
|
1054
|
+
disabled.add(name);
|
|
1055
|
+
savePatchworkConfig({
|
|
1056
|
+
...cfg,
|
|
1057
|
+
recipes: {
|
|
1058
|
+
...cfg.recipes,
|
|
1059
|
+
disabled: [...disabled],
|
|
1060
|
+
},
|
|
1061
|
+
});
|
|
1062
|
+
return { ok: true };
|
|
1063
|
+
}
|
|
1064
|
+
catch (err) {
|
|
1065
|
+
return {
|
|
1066
|
+
ok: false,
|
|
1067
|
+
error: err instanceof Error ? err.message : String(err),
|
|
1068
|
+
};
|
|
1069
|
+
}
|
|
1070
|
+
};
|
|
1010
1071
|
this.server.runsFn = (q) => {
|
|
1011
1072
|
if (!this.recipeRunLog)
|
|
1012
1073
|
return [];
|
|
@@ -1022,14 +1083,28 @@ export class Bridge {
|
|
|
1022
1083
|
...(q.after !== undefined && { after: q.after }),
|
|
1023
1084
|
});
|
|
1024
1085
|
};
|
|
1025
|
-
this.server.
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1086
|
+
this.server.runDetailFn = (seq) => {
|
|
1087
|
+
if (!this.recipeRunLog)
|
|
1088
|
+
return null;
|
|
1089
|
+
return this.recipeRunLog.getBySeq(seq);
|
|
1090
|
+
};
|
|
1091
|
+
this.server.runPlanFn = async (recipeName) => {
|
|
1092
|
+
const { runRecipeDryPlan } = await import("./commands/recipe.js");
|
|
1093
|
+
return (await runRecipeDryPlan(recipeName));
|
|
1094
|
+
};
|
|
1095
|
+
this.server.sessionsFn = () => [...this.sessions.values()].map((s) => {
|
|
1096
|
+
const tools = this.activityLog.querySessionTools(s.id, 1);
|
|
1097
|
+
return {
|
|
1098
|
+
id: s.id,
|
|
1099
|
+
connectedAt: new Date(s.connectedAt).toISOString(),
|
|
1100
|
+
openedFileCount: s.openedFiles.size,
|
|
1101
|
+
pendingApprovals: getApprovalQueue()
|
|
1102
|
+
.list()
|
|
1103
|
+
.filter((a) => a.sessionId === s.id).length,
|
|
1104
|
+
firstTool: tools[0]?.tool,
|
|
1105
|
+
remoteAddr: s.remoteAddr,
|
|
1106
|
+
};
|
|
1107
|
+
});
|
|
1033
1108
|
this.server.sessionDetailFn = (id) => {
|
|
1034
1109
|
const s = this.sessions.get(id);
|
|
1035
1110
|
const summary = s
|
|
@@ -1068,7 +1143,36 @@ export class Bridge {
|
|
|
1068
1143
|
if (!match) {
|
|
1069
1144
|
return { ok: false, error: "not_found" };
|
|
1070
1145
|
}
|
|
1071
|
-
|
|
1146
|
+
if (match.format === "yaml") {
|
|
1147
|
+
let payloadText;
|
|
1148
|
+
if (payload !== undefined) {
|
|
1149
|
+
try {
|
|
1150
|
+
payloadText = JSON.stringify(payload);
|
|
1151
|
+
}
|
|
1152
|
+
catch {
|
|
1153
|
+
payloadText = String(payload);
|
|
1154
|
+
}
|
|
1155
|
+
if (payloadText.length > 8_000) {
|
|
1156
|
+
payloadText = `${payloadText.slice(0, 8_000)}\n…[truncated]`;
|
|
1157
|
+
}
|
|
1158
|
+
}
|
|
1159
|
+
const seedContext = {
|
|
1160
|
+
hook_path: hookPath,
|
|
1161
|
+
webhook_path: hookPath,
|
|
1162
|
+
...(payloadText !== undefined
|
|
1163
|
+
? { payload: payloadText, webhook_payload: payloadText }
|
|
1164
|
+
: {}),
|
|
1165
|
+
};
|
|
1166
|
+
return this._fireYamlRecipe({
|
|
1167
|
+
filePath: match.filePath,
|
|
1168
|
+
name: match.name,
|
|
1169
|
+
taskIdPrefix: `yaml-webhook-${match.name}`,
|
|
1170
|
+
triggerSourceSuffix: `webhook:${match.name}`,
|
|
1171
|
+
logLabel: `webhook "${match.name}"`,
|
|
1172
|
+
seedContext,
|
|
1173
|
+
});
|
|
1174
|
+
}
|
|
1175
|
+
const loaded = loadRecipePrompt(recipesDir, path.basename(match.filePath, path.extname(match.filePath)));
|
|
1072
1176
|
if (!loaded) {
|
|
1073
1177
|
return { ok: false, error: "recipe_file_missing" };
|
|
1074
1178
|
}
|
|
@@ -1086,7 +1190,7 @@ export class Bridge {
|
|
|
1086
1190
|
};
|
|
1087
1191
|
}
|
|
1088
1192
|
};
|
|
1089
|
-
this.server.runRecipeFn = async (name) => {
|
|
1193
|
+
this.server.runRecipeFn = async (name, vars) => {
|
|
1090
1194
|
if (!this.orchestrator) {
|
|
1091
1195
|
return {
|
|
1092
1196
|
ok: false,
|
|
@@ -1094,26 +1198,45 @@ export class Bridge {
|
|
|
1094
1198
|
};
|
|
1095
1199
|
}
|
|
1096
1200
|
const recipesDir = path.join(os.homedir(), ".patchwork", "recipes");
|
|
1201
|
+
// Try JSON recipe first (legacy path: enqueue prompt as a task).
|
|
1097
1202
|
const loaded = loadRecipePrompt(recipesDir, name);
|
|
1098
|
-
if (
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1203
|
+
if (loaded) {
|
|
1204
|
+
try {
|
|
1205
|
+
let prompt = loaded.prompt;
|
|
1206
|
+
if (vars && Object.keys(vars).length > 0) {
|
|
1207
|
+
const varLines = Object.entries(vars)
|
|
1208
|
+
.map(([k, v]) => `${k}=${v}`)
|
|
1209
|
+
.join("\n");
|
|
1210
|
+
prompt = `Variables:\n${varLines}\n\n${prompt}`;
|
|
1211
|
+
}
|
|
1212
|
+
const taskId = this.orchestrator.enqueue({
|
|
1213
|
+
prompt,
|
|
1214
|
+
triggerSource: `recipe:${name}`,
|
|
1215
|
+
});
|
|
1216
|
+
return { ok: true, taskId };
|
|
1217
|
+
}
|
|
1218
|
+
catch (err) {
|
|
1219
|
+
return {
|
|
1220
|
+
ok: false,
|
|
1221
|
+
error: err instanceof Error ? err.message : String(err),
|
|
1222
|
+
};
|
|
1223
|
+
}
|
|
1110
1224
|
}
|
|
1111
|
-
|
|
1225
|
+
// Fall through to YAML runner for .yaml/.yml recipes.
|
|
1226
|
+
const ymlPath = findYamlRecipePath(recipesDir, name);
|
|
1227
|
+
if (!ymlPath) {
|
|
1112
1228
|
return {
|
|
1113
1229
|
ok: false,
|
|
1114
|
-
error:
|
|
1230
|
+
error: `Recipe "${name}" not found in ${recipesDir}`,
|
|
1115
1231
|
};
|
|
1116
1232
|
}
|
|
1233
|
+
return this._fireYamlRecipe({
|
|
1234
|
+
filePath: ymlPath,
|
|
1235
|
+
name,
|
|
1236
|
+
taskIdPrefix: `yaml-recipe-${name}`,
|
|
1237
|
+
triggerSourceSuffix: `recipe:${name}`,
|
|
1238
|
+
logLabel: `"${name}"`,
|
|
1239
|
+
});
|
|
1117
1240
|
};
|
|
1118
1241
|
this.server.readyFn = () => {
|
|
1119
1242
|
// Count tools from the first active session (all sessions share the same tool set)
|
|
@@ -1156,7 +1279,10 @@ export class Bridge {
|
|
|
1156
1279
|
workspace: this.config.workspace,
|
|
1157
1280
|
approvalGate: this.server.approvalGate,
|
|
1158
1281
|
fullMode: this.config.fullMode,
|
|
1159
|
-
|
|
1282
|
+
driver: this.config.driver,
|
|
1283
|
+
model: loadPatchworkConfig().model,
|
|
1284
|
+
localEndpoint: loadPatchworkConfig().localEndpoint,
|
|
1285
|
+
localModel: loadPatchworkConfig().localModel,
|
|
1160
1286
|
automationEnabled: this.config.automationEnabled,
|
|
1161
1287
|
port: this.port,
|
|
1162
1288
|
webhookUrl: this.server.approvalWebhookUrl ?? null,
|
|
@@ -1262,6 +1388,13 @@ export class Bridge {
|
|
|
1262
1388
|
throw err;
|
|
1263
1389
|
}
|
|
1264
1390
|
this.port = port;
|
|
1391
|
+
// 4a-deferred. Start recipe scheduler now that port is known (bridgeMcp needs a valid port).
|
|
1392
|
+
if (this.recipeScheduler) {
|
|
1393
|
+
const scheduled = this.recipeScheduler.start();
|
|
1394
|
+
if (scheduled.length > 0) {
|
|
1395
|
+
this.logger.info(`[patchwork] scheduled ${scheduled.length} cron recipe${scheduled.length === 1 ? "" : "s"}`);
|
|
1396
|
+
}
|
|
1397
|
+
}
|
|
1265
1398
|
// 4b. Start WebSocket keepalive heartbeat (keeps MCP session alive during long idle periods)
|
|
1266
1399
|
this._startWsHeartbeat();
|
|
1267
1400
|
// 4c. Load persisted tasks from previous sessions (best-effort)
|
|
@@ -1392,6 +1525,63 @@ export class Bridge {
|
|
|
1392
1525
|
})}`);
|
|
1393
1526
|
}
|
|
1394
1527
|
}
|
|
1528
|
+
/**
|
|
1529
|
+
* Load and fire a YAML recipe in the background via the orchestrator.
|
|
1530
|
+
* Returns `{ ok, taskId, name? }` immediately; execution continues async.
|
|
1531
|
+
* Both the webhook path and runRecipeFn use this to eliminate duplication.
|
|
1532
|
+
*/
|
|
1533
|
+
async _fireYamlRecipe(opts) {
|
|
1534
|
+
if (!this.recipeOrchestrator) {
|
|
1535
|
+
return { ok: false, error: "recipe orchestrator unavailable" };
|
|
1536
|
+
}
|
|
1537
|
+
const orch = this.orchestrator;
|
|
1538
|
+
const { buildChainedDeps, dispatchRecipe } = await import("./recipes/yamlRunner.js");
|
|
1539
|
+
const claudeCodeFn = async (prompt) => {
|
|
1540
|
+
const task = await orch.runAndWait({
|
|
1541
|
+
prompt,
|
|
1542
|
+
triggerSource: `${opts.triggerSourceSuffix}:agent`,
|
|
1543
|
+
timeoutMs: 600_000,
|
|
1544
|
+
});
|
|
1545
|
+
return task.output ?? task.errorMessage ?? "";
|
|
1546
|
+
};
|
|
1547
|
+
const runnerDeps = { workdir: this.config.workspace, claudeCodeFn };
|
|
1548
|
+
const chainedOptions = {
|
|
1549
|
+
sourcePath: opts.filePath,
|
|
1550
|
+
runLogDir: this.recipeRunLog
|
|
1551
|
+
? path.join(os.homedir(), ".patchwork")
|
|
1552
|
+
: undefined,
|
|
1553
|
+
};
|
|
1554
|
+
const fireResult = await this.recipeOrchestrator
|
|
1555
|
+
.fire({
|
|
1556
|
+
filePath: opts.filePath,
|
|
1557
|
+
name: opts.name,
|
|
1558
|
+
triggerSource: opts.triggerSourceSuffix,
|
|
1559
|
+
seedContext: opts.seedContext,
|
|
1560
|
+
dispatchFn: async (recipe, _deps, seedContext) => {
|
|
1561
|
+
const result = await dispatchRecipe(recipe, {
|
|
1562
|
+
...runnerDeps,
|
|
1563
|
+
chainedDeps: buildChainedDeps(runnerDeps, claudeCodeFn),
|
|
1564
|
+
chainedOptions,
|
|
1565
|
+
}, seedContext);
|
|
1566
|
+
const steps = "stepsRun" in result
|
|
1567
|
+
? result.stepsRun
|
|
1568
|
+
: (result.summary?.total ?? "?");
|
|
1569
|
+
const succeeded = "stepsRun" in result ? !result.errorMessage : result.success;
|
|
1570
|
+
if (succeeded)
|
|
1571
|
+
recordRecipeRun();
|
|
1572
|
+
this.logger.info?.(`[recipe] ${opts.logLabel} finished: ${steps} steps`);
|
|
1573
|
+
return result;
|
|
1574
|
+
},
|
|
1575
|
+
})
|
|
1576
|
+
.catch((err) => {
|
|
1577
|
+
this.logger.warn?.(`[recipe] ${opts.logLabel} error: ${err instanceof Error ? err.message : String(err)}`);
|
|
1578
|
+
return {
|
|
1579
|
+
ok: false,
|
|
1580
|
+
error: err instanceof Error ? err.message : String(err),
|
|
1581
|
+
};
|
|
1582
|
+
});
|
|
1583
|
+
return fireResult;
|
|
1584
|
+
}
|
|
1395
1585
|
/** Start the bridge-level WebSocket keepalive heartbeat. Idempotent. */
|
|
1396
1586
|
_startWsHeartbeat() {
|
|
1397
1587
|
if (this.wsHeartbeatInterval || this.config.wsPingIntervalMs === 0)
|
|
@@ -1458,7 +1648,7 @@ export class Bridge {
|
|
|
1458
1648
|
if (this.checkpoint && this.port > 0) {
|
|
1459
1649
|
try {
|
|
1460
1650
|
await Promise.race([
|
|
1461
|
-
Promise.resolve().then(() => this.checkpoint
|
|
1651
|
+
Promise.resolve().then(() => this.checkpoint?.write(this._buildCheckpoint(this.port))),
|
|
1462
1652
|
new Promise((resolve) => setTimeout(resolve, 3000)),
|
|
1463
1653
|
]);
|
|
1464
1654
|
}
|