patchwork-os 0.2.0-alpha.9 → 0.2.0-beta.0
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 +315 -35
- package/deploy/bootstrap-new-vps.sh +12 -12
- package/deploy/bootstrap-vps.sh +187 -0
- package/deploy/deploy-dashboard.sh +174 -0
- package/deploy/deploy-landing.sh +136 -0
- package/dist/activationMetrics.d.ts +67 -0
- package/dist/activationMetrics.js +255 -0
- package/dist/activationMetrics.js.map +1 -0
- package/dist/activityLog.d.ts +49 -0
- package/dist/activityLog.js +78 -0
- package/dist/activityLog.js.map +1 -1
- package/dist/approvalHttp.d.ts +49 -2
- package/dist/approvalHttp.js +217 -21
- package/dist/approvalHttp.js.map +1 -1
- package/dist/approvalInsights.d.ts +49 -0
- package/dist/approvalInsights.js +97 -0
- package/dist/approvalInsights.js.map +1 -0
- package/dist/approvalQueue.d.ts +27 -1
- package/dist/approvalQueue.js +123 -3
- package/dist/approvalQueue.js.map +1 -1
- package/dist/approvalSignals.d.ts +124 -0
- package/dist/approvalSignals.js +512 -0
- package/dist/approvalSignals.js.map +1 -0
- package/dist/automation.d.ts +57 -0
- package/dist/automation.js +156 -59
- package/dist/automation.js.map +1 -1
- package/dist/automationSuggestions.d.ts +79 -0
- package/dist/automationSuggestions.js +150 -0
- package/dist/automationSuggestions.js.map +1 -0
- package/dist/bridge.d.ts +3 -0
- package/dist/bridge.js +174 -143
- package/dist/bridge.js.map +1 -1
- package/dist/bridgeToken.js +57 -19
- package/dist/bridgeToken.js.map +1 -1
- package/dist/ccPermissions.d.ts +15 -0
- package/dist/ccPermissions.js +21 -4
- package/dist/ccPermissions.js.map +1 -1
- package/dist/claudeDriver.js +74 -16
- package/dist/claudeDriver.js.map +1 -1
- package/dist/claudeOrchestrator.d.ts +1 -1
- package/dist/claudeOrchestrator.js +14 -8
- package/dist/claudeOrchestrator.js.map +1 -1
- package/dist/commands/dashboard.js +1 -1
- package/dist/commands/dashboard.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/patchworkInit.d.ts +8 -0
- package/dist/commands/patchworkInit.js +77 -11
- package/dist/commands/patchworkInit.js.map +1 -1
- package/dist/commands/recipe.d.ts +289 -0
- package/dist/commands/recipe.js +1359 -0
- package/dist/commands/recipe.js.map +1 -0
- package/dist/commands/recipeInstall.d.ts +150 -0
- package/dist/commands/recipeInstall.js +647 -0
- package/dist/commands/recipeInstall.js.map +1 -0
- package/dist/commands/tracesExport.d.ts +83 -0
- package/dist/commands/tracesExport.js +269 -0
- package/dist/commands/tracesExport.js.map +1 -0
- package/dist/commands/tracesImport.d.ts +56 -0
- package/dist/commands/tracesImport.js +161 -0
- package/dist/commands/tracesImport.js.map +1 -0
- package/dist/config.d.ts +22 -1
- package/dist/config.js +108 -9
- package/dist/config.js.map +1 -1
- package/dist/connectorRoutes.d.ts +43 -0
- package/dist/connectorRoutes.js +1609 -0
- package/dist/connectorRoutes.js.map +1 -0
- package/dist/connectors/asana.d.ts +198 -0
- package/dist/connectors/asana.js +679 -0
- package/dist/connectors/asana.js.map +1 -0
- package/dist/connectors/baseConnector.d.ts +153 -0
- package/dist/connectors/baseConnector.js +336 -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/discord.d.ts +150 -0
- package/dist/connectors/discord.js +543 -0
- package/dist/connectors/discord.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.js +17 -18
- package/dist/connectors/github.js.map +1 -1
- package/dist/connectors/gitlab.d.ts +180 -0
- package/dist/connectors/gitlab.js +582 -0
- package/dist/connectors/gitlab.js.map +1 -0
- package/dist/connectors/gmail.d.ts +4 -1
- package/dist/connectors/gmail.js +149 -27
- package/dist/connectors/gmail.js.map +1 -1
- package/dist/connectors/googleCalendar.d.ts +4 -1
- package/dist/connectors/googleCalendar.js +88 -25
- package/dist/connectors/googleCalendar.js.map +1 -1
- package/dist/connectors/googleDrive.d.ts +34 -0
- package/dist/connectors/googleDrive.js +321 -0
- package/dist/connectors/googleDrive.js.map +1 -0
- package/dist/connectors/htmlEscape.d.ts +5 -0
- package/dist/connectors/htmlEscape.js +13 -0
- package/dist/connectors/htmlEscape.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.js +30 -19
- package/dist/connectors/linear.js.map +1 -1
- package/dist/connectors/mcpOAuth.d.ts +3 -0
- package/dist/connectors/mcpOAuth.js +64 -10
- package/dist/connectors/mcpOAuth.js.map +1 -1
- 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/oauthStateStore.d.ts +31 -0
- package/dist/connectors/oauthStateStore.js +52 -0
- package/dist/connectors/oauthStateStore.js.map +1 -0
- package/dist/connectors/pagerduty.d.ts +160 -0
- package/dist/connectors/pagerduty.js +464 -0
- package/dist/connectors/pagerduty.js.map +1 -0
- package/dist/connectors/sentry.js +5 -13
- package/dist/connectors/sentry.js.map +1 -1
- package/dist/connectors/slack.d.ts +16 -1
- package/dist/connectors/slack.js +155 -32
- package/dist/connectors/slack.js.map +1 -1
- 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 +484 -0
- package/dist/connectors/tokenStorage.js.map +1 -0
- package/dist/connectors/zendesk.d.ts +104 -0
- package/dist/connectors/zendesk.js +442 -0
- package/dist/connectors/zendesk.js.map +1 -0
- package/dist/cors.d.ts +10 -0
- package/dist/cors.js +29 -0
- package/dist/cors.js.map +1 -0
- package/dist/decisionReplay.d.ts +72 -0
- package/dist/decisionReplay.js +92 -0
- package/dist/decisionReplay.js.map +1 -0
- package/dist/decisionTraceLog.d.ts +6 -0
- package/dist/decisionTraceLog.js +54 -2
- package/dist/decisionTraceLog.js.map +1 -1
- 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 +79 -0
- package/dist/featureFlags.js +208 -0
- package/dist/featureFlags.js.map +1 -0
- package/dist/fp/automationInterpreter.js +26 -21
- 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/automationState.js +4 -1
- package/dist/fp/automationState.js.map +1 -1
- package/dist/fp/policyParser.js +21 -1
- package/dist/fp/policyParser.js.map +1 -1
- package/dist/inboxRoutes.d.ts +22 -0
- package/dist/inboxRoutes.js +114 -0
- package/dist/inboxRoutes.js.map +1 -0
- package/dist/index.js +1400 -201
- 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/mcpRoutes.d.ts +37 -0
- package/dist/mcpRoutes.js +76 -0
- package/dist/mcpRoutes.js.map +1 -0
- package/dist/oauth.d.ts +7 -1
- package/dist/oauth.js +201 -39
- package/dist/oauth.js.map +1 -1
- package/dist/oauthRoutes.d.ts +32 -0
- package/dist/oauthRoutes.js +124 -0
- package/dist/oauthRoutes.js.map +1 -0
- package/dist/orchestrator/orchestratorBridge.js +2 -2
- package/dist/orchestrator/orchestratorBridge.js.map +1 -1
- package/dist/patchworkConfig.d.ts +16 -0
- package/dist/patchworkConfig.js +1 -1
- package/dist/patchworkConfig.js.map +1 -1
- package/dist/pluginLoader.d.ts +28 -0
- package/dist/pluginLoader.js +77 -11
- package/dist/pluginLoader.js.map +1 -1
- package/dist/pluginWatcher.js +8 -3
- package/dist/pluginWatcher.js.map +1 -1
- package/dist/preToolUseHook.d.ts +12 -0
- package/dist/preToolUseHook.js +23 -0
- package/dist/preToolUseHook.js.map +1 -1
- package/dist/recipeOrchestration.d.ts +121 -0
- package/dist/recipeOrchestration.js +955 -0
- package/dist/recipeOrchestration.js.map +1 -0
- package/dist/recipeRoutes.d.ts +180 -0
- package/dist/recipeRoutes.js +1345 -0
- package/dist/recipeRoutes.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 +29 -0
- package/dist/recipes/agentExecutor.js +49 -0
- package/dist/recipes/agentExecutor.js.map +1 -0
- package/dist/recipes/chainedRunner.d.ts +191 -0
- package/dist/recipes/chainedRunner.js +759 -0
- package/dist/recipes/chainedRunner.js.map +1 -0
- package/dist/recipes/compiler.js +3 -3
- package/dist/recipes/compiler.js.map +1 -1
- 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/disabledMarkers.d.ts +48 -0
- package/dist/recipes/disabledMarkers.js +52 -0
- package/dist/recipes/disabledMarkers.js.map +1 -0
- package/dist/recipes/installer.js +3 -3
- package/dist/recipes/installer.js.map +1 -1
- package/dist/recipes/legacyRecipeCompat.d.ts +10 -0
- package/dist/recipes/legacyRecipeCompat.js +131 -0
- package/dist/recipes/legacyRecipeCompat.js.map +1 -0
- package/dist/recipes/manifest.d.ts +47 -0
- package/dist/recipes/manifest.js +156 -0
- package/dist/recipes/manifest.js.map +1 -0
- package/dist/recipes/migrationWarnings.d.ts +12 -0
- package/dist/recipes/migrationWarnings.js +44 -0
- package/dist/recipes/migrationWarnings.js.map +1 -0
- package/dist/recipes/migrations/index.d.ts +24 -0
- package/dist/recipes/migrations/index.js +55 -0
- package/dist/recipes/migrations/index.js.map +1 -0
- package/dist/recipes/migrations/types.d.ts +28 -0
- package/dist/recipes/migrations/types.js +2 -0
- package/dist/recipes/migrations/types.js.map +1 -0
- package/dist/recipes/migrations/v1.d.ts +11 -0
- package/dist/recipes/migrations/v1.js +18 -0
- package/dist/recipes/migrations/v1.js.map +1 -0
- package/dist/recipes/names.d.ts +40 -0
- package/dist/recipes/names.js +66 -0
- package/dist/recipes/names.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/parser.js +4 -1
- package/dist/recipes/parser.js.map +1 -1
- package/dist/recipes/replayRun.d.ts +62 -0
- package/dist/recipes/replayRun.js +97 -0
- package/dist/recipes/replayRun.js.map +1 -0
- package/dist/recipes/resolveRecipePath.d.ts +69 -0
- package/dist/recipes/resolveRecipePath.js +202 -0
- package/dist/recipes/resolveRecipePath.js.map +1 -0
- package/dist/recipes/scheduler.d.ts +23 -7
- package/dist/recipes/scheduler.js +225 -45
- 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/stepObservation.d.ts +44 -0
- package/dist/recipes/stepObservation.js +232 -0
- package/dist/recipes/stepObservation.js.map +1 -0
- package/dist/recipes/templateEngine.d.ts +62 -0
- package/dist/recipes/templateEngine.js +201 -0
- package/dist/recipes/templateEngine.js.map +1 -0
- package/dist/recipes/toolRegistry.d.ts +186 -0
- package/dist/recipes/toolRegistry.js +309 -0
- package/dist/recipes/toolRegistry.js.map +1 -0
- package/dist/recipes/tools/asana.d.ts +16 -0
- package/dist/recipes/tools/asana.js +524 -0
- package/dist/recipes/tools/asana.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/discord.d.ts +18 -0
- package/dist/recipes/tools/discord.js +254 -0
- package/dist/recipes/tools/discord.js.map +1 -0
- package/dist/recipes/tools/file.d.ts +12 -0
- package/dist/recipes/tools/file.js +174 -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 +116 -0
- package/dist/recipes/tools/github.js.map +1 -0
- package/dist/recipes/tools/gitlab.d.ts +11 -0
- package/dist/recipes/tools/gitlab.js +285 -0
- package/dist/recipes/tools/gitlab.js.map +1 -0
- package/dist/recipes/tools/gmail.d.ts +6 -0
- package/dist/recipes/tools/gmail.js +434 -0
- package/dist/recipes/tools/gmail.js.map +1 -0
- package/dist/recipes/tools/googleDrive.d.ts +1 -0
- package/dist/recipes/tools/googleDrive.js +55 -0
- package/dist/recipes/tools/googleDrive.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 +30 -0
- package/dist/recipes/tools/index.js +33 -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/jira.d.ts +14 -0
- package/dist/recipes/tools/jira.js +369 -0
- package/dist/recipes/tools/jira.js.map +1 -0
- package/dist/recipes/tools/linear.d.ts +7 -0
- package/dist/recipes/tools/linear.js +307 -0
- package/dist/recipes/tools/linear.js.map +1 -0
- package/dist/recipes/tools/meetingNotes.d.ts +21 -0
- package/dist/recipes/tools/meetingNotes.js +701 -0
- package/dist/recipes/tools/meetingNotes.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/pagerduty.d.ts +15 -0
- package/dist/recipes/tools/pagerduty.js +451 -0
- package/dist/recipes/tools/pagerduty.js.map +1 -0
- package/dist/recipes/tools/sentry.d.ts +12 -0
- package/dist/recipes/tools/sentry.js +73 -0
- package/dist/recipes/tools/sentry.js.map +1 -0
- package/dist/recipes/tools/slack.d.ts +6 -0
- package/dist/recipes/tools/slack.js +82 -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 +617 -0
- package/dist/recipes/validation.js.map +1 -0
- package/dist/recipes/yamlRunner.d.ts +116 -1
- package/dist/recipes/yamlRunner.js +1000 -401
- package/dist/recipes/yamlRunner.js.map +1 -1
- package/dist/recipesHttp.d.ts +137 -6
- package/dist/recipesHttp.js +941 -29
- package/dist/recipesHttp.js.map +1 -1
- package/dist/riskTier.js +7 -1
- package/dist/riskTier.js.map +1 -1
- package/dist/runLog.d.ts +100 -1
- package/dist/runLog.js +258 -5
- 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 +121 -8
- package/dist/server.js +538 -735
- package/dist/server.js.map +1 -1
- package/dist/ssrfGuard.d.ts +54 -0
- package/dist/ssrfGuard.js +122 -0
- package/dist/ssrfGuard.js.map +1 -0
- package/dist/streamableHttp.d.ts +39 -1
- package/dist/streamableHttp.js +128 -17
- package/dist/streamableHttp.js.map +1 -1
- package/dist/tokenUsageTracker.d.ts +33 -0
- package/dist/tokenUsageTracker.js +146 -0
- package/dist/tokenUsageTracker.js.map +1 -0
- package/dist/tools/activityLog.d.ts +2 -0
- package/dist/tools/addLinearComment.d.ts +1 -0
- package/dist/tools/addLinearComment.js +4 -2
- package/dist/tools/addLinearComment.js.map +1 -1
- package/dist/tools/batchLsp.d.ts +3 -0
- package/dist/tools/bridgeDoctor.d.ts +1 -0
- package/dist/tools/bridgeDoctor.js +2 -2
- package/dist/tools/bridgeDoctor.js.map +1 -1
- package/dist/tools/bridgeStatus.d.ts +1 -0
- package/dist/tools/cancelClaudeTask.d.ts +2 -0
- package/dist/tools/cancelClaudeTask.js +1 -0
- package/dist/tools/cancelClaudeTask.js.map +1 -1
- package/dist/tools/checkDocumentDirty.d.ts +1 -0
- package/dist/tools/clipboard.d.ts +2 -0
- package/dist/tools/closeTabs.d.ts +2 -0
- package/dist/tools/codeLens.d.ts +1 -0
- package/dist/tools/contextBundle.d.ts +1 -0
- package/dist/tools/createIssueFromAIComment.d.ts +1 -0
- package/dist/tools/createLinearIssue.d.ts +1 -0
- package/dist/tools/ctxGetTaskContext.d.ts +1 -0
- package/dist/tools/ctxQueryTraces.d.ts +1 -0
- package/dist/tools/ctxSaveTrace.d.ts +1 -0
- package/dist/tools/debug.d.ts +4 -0
- package/dist/tools/decorations.d.ts +2 -0
- package/dist/tools/documentLinks.d.ts +1 -0
- package/dist/tools/editText.d.ts +1 -0
- package/dist/tools/enrichCommit.d.ts +1 -0
- package/dist/tools/enrichStackTrace.d.ts +1 -0
- package/dist/tools/explainDiagnostic.d.ts +1 -0
- package/dist/tools/explainSymbol.d.ts +1 -0
- package/dist/tools/fetchCalendarEvents.d.ts +1 -0
- package/dist/tools/fetchGithubIssue.d.ts +1 -0
- package/dist/tools/fetchGithubPR.d.ts +1 -0
- package/dist/tools/fetchLinearIssue.d.ts +1 -0
- package/dist/tools/fetchSentryIssue.d.ts +1 -0
- package/dist/tools/fetchSlackProfile.d.ts +1 -0
- package/dist/tools/fetchSlackProfile.js +4 -1
- package/dist/tools/fetchSlackProfile.js.map +1 -1
- package/dist/tools/fileOperations.d.ts +3 -0
- package/dist/tools/fileWatcher.d.ts +2 -0
- package/dist/tools/findFiles.d.ts +1 -0
- package/dist/tools/findRelatedTests.d.ts +1 -0
- package/dist/tools/fixAllLintErrors.d.ts +1 -0
- package/dist/tools/foldingRanges.d.ts +1 -0
- package/dist/tools/formatDocument.d.ts +1 -0
- package/dist/tools/generateTests.d.ts +1 -0
- package/dist/tools/getAIComments.d.ts +1 -0
- package/dist/tools/getAnalyticsReport.d.ts +1 -0
- package/dist/tools/getArchitectureContext.d.ts +1 -0
- package/dist/tools/getBufferContent.d.ts +1 -0
- package/dist/tools/getChangeImpact.d.ts +1 -0
- package/dist/tools/getClaudeTaskStatus.d.ts +2 -0
- package/dist/tools/getClaudeTaskStatus.js +1 -0
- package/dist/tools/getClaudeTaskStatus.js.map +1 -1
- package/dist/tools/getCodeCoverage.d.ts +1 -0
- package/dist/tools/getCommitsForIssue.d.ts +1 -0
- package/dist/tools/getConnectorStatus.d.ts +1 -0
- package/dist/tools/getCurrentSelection.d.ts +2 -0
- package/dist/tools/getDebugState.d.ts +1 -0
- package/dist/tools/getDependencyTree.d.ts +1 -0
- package/dist/tools/getDiagnostics.d.ts +1 -0
- package/dist/tools/getDiffFromHandoff.d.ts +1 -0
- package/dist/tools/getDocumentSymbols.d.ts +25 -0
- package/dist/tools/getDocumentSymbols.js +74 -8
- package/dist/tools/getDocumentSymbols.js.map +1 -1
- package/dist/tools/getFileTree.d.ts +1 -0
- package/dist/tools/getGitDiff.d.ts +1 -0
- package/dist/tools/getGitHotspots.d.ts +1 -0
- package/dist/tools/getGitLog.d.ts +1 -0
- package/dist/tools/getGitStatus.d.ts +1 -0
- package/dist/tools/getImportTree.d.ts +1 -0
- package/dist/tools/getImportedSignatures.d.ts +1 -0
- package/dist/tools/getOpenEditors.d.ts +1 -0
- package/dist/tools/getPRTemplate.d.ts +1 -0
- package/dist/tools/getProjectContext.d.ts +1 -0
- package/dist/tools/getProjectInfo.d.ts +1 -0
- package/dist/tools/getSecurityAdvisories.d.ts +1 -0
- package/dist/tools/getSecurityAdvisories.js +10 -1
- package/dist/tools/getSecurityAdvisories.js.map +1 -1
- package/dist/tools/getSessionUsage.d.ts +4 -0
- package/dist/tools/getSessionUsage.js +3 -0
- package/dist/tools/getSessionUsage.js.map +1 -1
- package/dist/tools/getSymbolHistory.d.ts +1 -0
- package/dist/tools/getToolCapabilities.d.ts +1 -0
- package/dist/tools/getTypeSignature.d.ts +1 -0
- package/dist/tools/getWorkspaceFolders.d.ts +1 -0
- package/dist/tools/getWorkspaceSettings.d.ts +1 -0
- package/dist/tools/gitHistory.d.ts +2 -0
- package/dist/tools/gitWrite.d.ts +11 -0
- package/dist/tools/github/actions.d.ts +2 -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 +342 -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 +1 -0
- package/dist/tools/github/index.js +1 -0
- package/dist/tools/github/index.js.map +1 -1
- package/dist/tools/github/issues.d.ts +4 -0
- package/dist/tools/github/issues.js +8 -4
- package/dist/tools/github/issues.js.map +1 -1
- package/dist/tools/github/pr.d.ts +7 -0
- package/dist/tools/github/pr.js +50 -12
- package/dist/tools/github/pr.js.map +1 -1
- package/dist/tools/handoffNote.d.ts +4 -0
- package/dist/tools/handoffNote.js +2 -0
- package/dist/tools/handoffNote.js.map +1 -1
- package/dist/tools/hoverAtCursor.d.ts +1 -0
- package/dist/tools/httpClient.d.ts +2 -0
- package/dist/tools/index.d.ts +8 -0
- package/dist/tools/index.js +47 -8
- package/dist/tools/index.js.map +1 -1
- package/dist/tools/inlayHints.d.ts +1 -0
- package/dist/tools/launchQuickTask.d.ts +2 -0
- package/dist/tools/launchQuickTask.js +1 -0
- package/dist/tools/launchQuickTask.js.map +1 -1
- package/dist/tools/listClaudeTasks.d.ts +2 -0
- package/dist/tools/listClaudeTasks.js +1 -0
- package/dist/tools/listClaudeTasks.js.map +1 -1
- package/dist/tools/listTerminals.d.ts +1 -0
- package/dist/tools/lsp.d.ts +14 -0
- package/dist/tools/navigateToSymbolByName.d.ts +1 -0
- package/dist/tools/openDiff.d.ts +1 -0
- package/dist/tools/openFile.d.ts +1 -0
- package/dist/tools/openInBrowser.d.ts +1 -0
- package/dist/tools/organizeImports.d.ts +1 -0
- package/dist/tools/performanceReport.d.ts +1 -0
- package/dist/tools/planPersistence.d.ts +5 -0
- package/dist/tools/previewEdit.d.ts +1 -0
- package/dist/tools/refactorAnalyze.d.ts +1 -0
- package/dist/tools/refactorPreview.d.ts +2 -0
- package/dist/tools/refactorPreview.js +1 -0
- package/dist/tools/refactorPreview.js.map +1 -1
- package/dist/tools/replaceBlock.d.ts +1 -0
- package/dist/tools/resumeClaudeTask.d.ts +2 -0
- package/dist/tools/resumeClaudeTask.js +1 -0
- package/dist/tools/resumeClaudeTask.js.map +1 -1
- package/dist/tools/runClaudeTask.d.ts +2 -0
- package/dist/tools/runClaudeTask.js +1 -0
- package/dist/tools/runClaudeTask.js.map +1 -1
- package/dist/tools/runCommand.d.ts +1 -0
- package/dist/tools/runTests.d.ts +1 -0
- package/dist/tools/saveDocument.d.ts +1 -0
- package/dist/tools/screenshotAndAnnotate.d.ts +1 -0
- package/dist/tools/searchAndReplace.d.ts +1 -0
- package/dist/tools/searchTools.d.ts +1 -0
- package/dist/tools/searchTools.js +1 -1
- package/dist/tools/searchTools.js.map +1 -1
- package/dist/tools/searchWorkspace.d.ts +1 -0
- package/dist/tools/selectionRanges.d.ts +1 -0
- package/dist/tools/semanticTokens.d.ts +1 -0
- package/dist/tools/setActiveWorkspaceFolder.d.ts +1 -0
- package/dist/tools/signatureHelp.d.ts +1 -0
- package/dist/tools/slackListChannels.d.ts +1 -0
- package/dist/tools/slackListChannels.js.map +1 -1
- package/dist/tools/slackPostMessage.d.ts +1 -0
- package/dist/tools/slackPostMessage.js +11 -6
- package/dist/tools/slackPostMessage.js.map +1 -1
- package/dist/tools/terminal.d.ts +6 -0
- package/dist/tools/testTraceToSource.d.ts +1 -0
- package/dist/tools/testTraceToSource.js +2 -2
- package/dist/tools/testTraceToSource.js.map +1 -1
- package/dist/tools/transaction.d.ts +23 -0
- package/dist/tools/transaction.js +29 -0
- package/dist/tools/transaction.js.map +1 -1
- package/dist/tools/typeHierarchy.d.ts +1 -0
- package/dist/tools/updateLinearIssue.d.ts +1 -0
- package/dist/tools/updateLinearIssue.js +20 -6
- package/dist/tools/updateLinearIssue.js.map +1 -1
- package/dist/tools/utils.d.ts +2 -0
- package/dist/tools/utils.js.map +1 -1
- package/dist/tools/vscodeCommands.d.ts +2 -0
- package/dist/tools/vscodeTasks.d.ts +2 -0
- package/dist/tools/workspaceSettings.d.ts +1 -0
- package/dist/traceEncryption.d.ts +46 -0
- package/dist/traceEncryption.js +124 -0
- package/dist/traceEncryption.js.map +1 -0
- package/dist/transport.d.ts +46 -1
- package/dist/transport.js +173 -19
- package/dist/transport.js.map +1 -1
- package/package.json +30 -8
- package/scripts/mcp-stdio-shim.cjs +19 -3
- package/scripts/start-all.sh +30 -1
- 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/policies/README.md +72 -0
- package/templates/policies/conservative.json +14 -0
- package/templates/policies/developer.json +14 -0
- package/templates/policies/headless-ci.json +24 -0
- package/templates/policies/personal-assistant.json +15 -0
- package/templates/policies/regulated-industry.json +18 -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 +2 -2
- package/templates/recipes/project-health-check.yaml +50 -0
- package/templates/recipes/webhook/README.md +70 -0
- package/templates/recipes/webhook/capture-thought.yaml +26 -0
- package/templates/recipes/webhook/customer-escalation.yaml +49 -0
- package/templates/recipes/webhook/incident-intake.yaml +46 -0
- package/templates/recipes/webhook/meeting-prep.yaml +48 -0
- package/templates/recipes/webhook/morning-brief.yaml +57 -0
package/dist/recipesHttp.js
CHANGED
|
@@ -1,8 +1,268 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { existsSync, mkdirSync, readdirSync, readFileSync, rmSync, statSync, writeFileSync, } from "node:fs";
|
|
3
|
+
import { homedir } from "node:os";
|
|
4
|
+
import * as path from "node:path";
|
|
5
|
+
import { parse as parseYaml, stringify as stringifyYaml } from "yaml";
|
|
6
|
+
import { loadConfig, saveConfig as savePatchworkConfig, } from "./patchworkConfig.js";
|
|
7
|
+
import { disabledMarkerPath, getConfigDisabledNames, isInstallDirDisabled, } from "./recipes/disabledMarkers.js";
|
|
8
|
+
import { RECIPE_NAME_RE } from "./recipes/names.js";
|
|
9
|
+
import { validateRecipeDefinition } from "./recipes/validation.js";
|
|
10
|
+
/**
|
|
11
|
+
* Returns true unless `filePath` lives inside an install dir whose
|
|
12
|
+
* `.disabled` marker is present. Top-level legacy recipes (direct children
|
|
13
|
+
* of `recipesDir`) are always considered enabled — there's no install dir
|
|
14
|
+
* to put a marker in. Used by every trigger surface (webhook, manual fire,
|
|
15
|
+
* automation) so the marker means the same thing everywhere.
|
|
16
|
+
*/
|
|
17
|
+
export function isRecipeFileEnabled(filePath, recipesDir) {
|
|
18
|
+
const rel = path.relative(recipesDir, filePath);
|
|
19
|
+
// Top-level file in recipesDir → no install dir → enabled by default.
|
|
20
|
+
if (rel === "" || rel.startsWith("..") || !rel.includes(path.sep)) {
|
|
21
|
+
return true;
|
|
22
|
+
}
|
|
23
|
+
const installDirName = rel.split(path.sep)[0];
|
|
24
|
+
if (!installDirName)
|
|
25
|
+
return true;
|
|
26
|
+
const installDir = path.join(recipesDir, installDirName);
|
|
27
|
+
return !isInstallDirDisabled(installDir);
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Iterate one level of subdirectories under `recipesDir` that look like
|
|
31
|
+
* install dirs (directory containing `recipe.json` or at least one `.yaml`).
|
|
32
|
+
* Skips dirs whose `.disabled` marker is present so callers automatically
|
|
33
|
+
* honor the marker without having to remember.
|
|
34
|
+
*
|
|
35
|
+
* Yields `{ installDir, entrypointPath }` pairs where `entrypointPath` is the
|
|
36
|
+
* file the caller should parse:
|
|
37
|
+
* - `recipe.json`'s `recipes.main` if a manifest exists
|
|
38
|
+
* - otherwise the first `*.yaml` / `*.yml` in the dir
|
|
39
|
+
*
|
|
40
|
+
* Used by webhook + manual-fire path resolvers to find recipes installed
|
|
41
|
+
* via `runRecipeInstall`.
|
|
42
|
+
*/
|
|
43
|
+
function* iterateInstallDirs(recipesDir, options = {}) {
|
|
44
|
+
const includeDisabled = options.includeDisabled === true;
|
|
45
|
+
let entries;
|
|
46
|
+
try {
|
|
47
|
+
entries = readdirSync(recipesDir);
|
|
48
|
+
}
|
|
49
|
+
catch {
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
for (const f of entries) {
|
|
53
|
+
const fullPath = path.join(recipesDir, f);
|
|
54
|
+
let isDir = false;
|
|
55
|
+
try {
|
|
56
|
+
isDir = statSync(fullPath).isDirectory();
|
|
57
|
+
}
|
|
58
|
+
catch {
|
|
59
|
+
continue;
|
|
60
|
+
}
|
|
61
|
+
if (!isDir)
|
|
62
|
+
continue;
|
|
63
|
+
const enabled = !isInstallDirDisabled(fullPath);
|
|
64
|
+
if (!enabled && !includeDisabled)
|
|
65
|
+
continue;
|
|
66
|
+
let entrypoint = null;
|
|
67
|
+
const manifestPath = path.join(fullPath, "recipe.json");
|
|
68
|
+
if (existsSync(manifestPath)) {
|
|
69
|
+
try {
|
|
70
|
+
const m = JSON.parse(readFileSync(manifestPath, "utf-8"));
|
|
71
|
+
if (m.recipes?.main) {
|
|
72
|
+
const candidate = path.join(fullPath, m.recipes.main);
|
|
73
|
+
if (existsSync(candidate))
|
|
74
|
+
entrypoint = candidate;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
catch {
|
|
78
|
+
// malformed manifest — fall through to first-yaml fallback
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
if (!entrypoint) {
|
|
82
|
+
try {
|
|
83
|
+
const yaml = readdirSync(fullPath).find((x) => /\.ya?ml$/i.test(x));
|
|
84
|
+
if (yaml)
|
|
85
|
+
entrypoint = path.join(fullPath, yaml);
|
|
86
|
+
}
|
|
87
|
+
catch {
|
|
88
|
+
// unreadable
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
if (entrypoint) {
|
|
92
|
+
yield { installDir: fullPath, entrypointPath: entrypoint, enabled };
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Locate an install dir by the *recipe name* declared inside its entrypoint
|
|
98
|
+
* (not the directory name). The dashboard reports recipes by the parsed
|
|
99
|
+
* `name` field, while `runRecipeEnable` looks them up by dir name —
|
|
100
|
+
* the two are usually different (`morning-pkg` vs `morning-brief`). Includes
|
|
101
|
+
* disabled dirs so re-enabling actually finds them.
|
|
102
|
+
*/
|
|
103
|
+
function findInstallDirByRecipeName(recipesDir, name) {
|
|
104
|
+
for (const { installDir, entrypointPath } of iterateInstallDirs(recipesDir, {
|
|
105
|
+
includeDisabled: true,
|
|
106
|
+
})) {
|
|
107
|
+
try {
|
|
108
|
+
const ext = path.extname(entrypointPath).toLowerCase();
|
|
109
|
+
const raw = readFileSync(entrypointPath, "utf-8");
|
|
110
|
+
const parsed = (ext === ".json" ? JSON.parse(raw) : parseYaml(raw));
|
|
111
|
+
if (parsed.name === name)
|
|
112
|
+
return installDir;
|
|
113
|
+
}
|
|
114
|
+
catch {
|
|
115
|
+
// skip malformed
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
return null;
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* Unified enable/disable for install-dir AND legacy top-level recipes.
|
|
122
|
+
*
|
|
123
|
+
* Routing:
|
|
124
|
+
* 1. Try to find an install dir whose entrypoint declares this `name`.
|
|
125
|
+
* If found, write/remove the `.disabled` marker on that dir. This
|
|
126
|
+
* matches CLI `recipe enable/disable` and the trigger-side
|
|
127
|
+
* enforcement landed in PRs #43 / #49.
|
|
128
|
+
* 2. Otherwise the recipe is a top-level legacy file — fall back to
|
|
129
|
+
* the legacy `cfg.recipes.disabled` config-file array, which the
|
|
130
|
+
* scheduler already honors as a parallel mechanism (it checks both).
|
|
131
|
+
*
|
|
132
|
+
* Replaces the old dashboard-only `setRecipeEnabledFn` that wrote ONLY to
|
|
133
|
+
* the legacy config — which silently did nothing for install-dir recipes.
|
|
134
|
+
*/
|
|
135
|
+
export function setRecipeEnabled(name, enabled, options = {}) {
|
|
136
|
+
const recipesDir = options.recipesDir ?? path.join(homedir(), ".patchwork", "recipes");
|
|
137
|
+
try {
|
|
138
|
+
const installDir = findInstallDirByRecipeName(recipesDir, name);
|
|
139
|
+
if (installDir) {
|
|
140
|
+
const markerPath = disabledMarkerPath(installDir);
|
|
141
|
+
if (enabled) {
|
|
142
|
+
if (existsSync(markerPath))
|
|
143
|
+
rmSync(markerPath);
|
|
144
|
+
}
|
|
145
|
+
else {
|
|
146
|
+
writeFileSync(markerPath, "");
|
|
147
|
+
}
|
|
148
|
+
return { ok: true };
|
|
149
|
+
}
|
|
150
|
+
// Legacy top-level path — fall back to config-file disabled list
|
|
151
|
+
const cfg = (options.loadConfigFn ?? loadConfig)();
|
|
152
|
+
const disabled = getConfigDisabledNames(cfg);
|
|
153
|
+
if (enabled)
|
|
154
|
+
disabled.delete(name);
|
|
155
|
+
else
|
|
156
|
+
disabled.add(name);
|
|
157
|
+
const next = {
|
|
158
|
+
...cfg,
|
|
159
|
+
recipes: {
|
|
160
|
+
...(cfg.recipes ?? {}),
|
|
161
|
+
disabled: [...disabled],
|
|
162
|
+
},
|
|
163
|
+
};
|
|
164
|
+
if (options.saveConfigFn)
|
|
165
|
+
options.saveConfigFn(next);
|
|
166
|
+
else
|
|
167
|
+
savePatchworkConfig(next);
|
|
168
|
+
return { ok: true };
|
|
169
|
+
}
|
|
170
|
+
catch (err) {
|
|
171
|
+
return {
|
|
172
|
+
ok: false,
|
|
173
|
+
error: err instanceof Error ? err.message : String(err),
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
function normalizeRecipeDraftTrigger(trigger) {
|
|
178
|
+
if (trigger.type === "schedule" || trigger.type === "cron") {
|
|
179
|
+
const schedule = typeof trigger.schedule === "string" && trigger.schedule.trim()
|
|
180
|
+
? trigger.schedule.trim()
|
|
181
|
+
: typeof trigger.cron === "string" && trigger.cron.trim()
|
|
182
|
+
? trigger.cron.trim()
|
|
183
|
+
: "";
|
|
184
|
+
return {
|
|
185
|
+
type: "cron",
|
|
186
|
+
...(schedule ? { schedule } : {}),
|
|
187
|
+
};
|
|
188
|
+
}
|
|
189
|
+
if (trigger.type === "webhook") {
|
|
190
|
+
const pathValue = typeof trigger.path === "string" ? trigger.path.trim() : "";
|
|
191
|
+
return {
|
|
192
|
+
type: "webhook",
|
|
193
|
+
...(pathValue ? { path: pathValue } : {}),
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
return { type: "manual" };
|
|
197
|
+
}
|
|
198
|
+
function validateRecipeDraft(draft) {
|
|
199
|
+
if (!draft || typeof draft !== "object") {
|
|
200
|
+
return "Invalid recipe draft";
|
|
201
|
+
}
|
|
202
|
+
if (!draft.trigger || typeof draft.trigger !== "object") {
|
|
203
|
+
return "trigger required";
|
|
204
|
+
}
|
|
205
|
+
if (draft.trigger.type !== "manual" &&
|
|
206
|
+
draft.trigger.type !== "webhook" &&
|
|
207
|
+
draft.trigger.type !== "schedule" &&
|
|
208
|
+
draft.trigger.type !== "cron") {
|
|
209
|
+
return "Invalid trigger type";
|
|
210
|
+
}
|
|
211
|
+
const normalizedTrigger = normalizeRecipeDraftTrigger(draft.trigger);
|
|
212
|
+
if (normalizedTrigger.type === "webhook") {
|
|
213
|
+
if (typeof normalizedTrigger.path !== "string" ||
|
|
214
|
+
!normalizedTrigger.path.startsWith("/")) {
|
|
215
|
+
return "webhook trigger requires a path starting with /";
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
if (normalizedTrigger.type === "cron") {
|
|
219
|
+
if (typeof normalizedTrigger.schedule !== "string" ||
|
|
220
|
+
!normalizedTrigger.schedule.trim()) {
|
|
221
|
+
return "cron trigger requires a schedule";
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
if (!Array.isArray(draft.steps) || draft.steps.length === 0) {
|
|
225
|
+
return "Recipe must have at least one step";
|
|
226
|
+
}
|
|
227
|
+
const stepIds = new Set();
|
|
228
|
+
for (let i = 0; i < draft.steps.length; i++) {
|
|
229
|
+
const step = draft.steps[i];
|
|
230
|
+
const index = i + 1;
|
|
231
|
+
const id = typeof step?.id === "string" ? step.id.trim() : "";
|
|
232
|
+
if (!id) {
|
|
233
|
+
return `Step ${index} is missing an id`;
|
|
234
|
+
}
|
|
235
|
+
if (stepIds.has(id)) {
|
|
236
|
+
return `Step ${index} has a duplicate id`;
|
|
237
|
+
}
|
|
238
|
+
stepIds.add(id);
|
|
239
|
+
if (typeof step?.prompt !== "string" || !step.prompt.trim()) {
|
|
240
|
+
return `Step ${index} is missing a prompt`;
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
if (draft.vars !== undefined) {
|
|
244
|
+
if (!Array.isArray(draft.vars)) {
|
|
245
|
+
return "vars must be an array";
|
|
246
|
+
}
|
|
247
|
+
const varNames = new Set();
|
|
248
|
+
for (let i = 0; i < draft.vars.length; i++) {
|
|
249
|
+
const item = draft.vars[i];
|
|
250
|
+
const index = i + 1;
|
|
251
|
+
const name = typeof item?.name === "string" ? item.name.trim() : "";
|
|
252
|
+
if (!name) {
|
|
253
|
+
return `Variable ${index} is missing a name`;
|
|
254
|
+
}
|
|
255
|
+
if (varNames.has(name)) {
|
|
256
|
+
return `Variable ${index} has a duplicate name`;
|
|
257
|
+
}
|
|
258
|
+
varNames.add(name);
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
return null;
|
|
262
|
+
}
|
|
3
263
|
export function saveRecipe(recipesDir, draft) {
|
|
4
264
|
const safeName = draft.name.toLowerCase().replace(/\s+/g, "-");
|
|
5
|
-
if (
|
|
265
|
+
if (!RECIPE_NAME_RE.test(safeName)) {
|
|
6
266
|
return { ok: false, error: "Invalid recipe name" };
|
|
7
267
|
}
|
|
8
268
|
const candidate = path.resolve(recipesDir, `${safeName}.json`);
|
|
@@ -10,20 +270,46 @@ export function saveRecipe(recipesDir, draft) {
|
|
|
10
270
|
if (!candidate.startsWith(base + path.sep)) {
|
|
11
271
|
return { ok: false, error: "Invalid path" };
|
|
12
272
|
}
|
|
273
|
+
const validationError = validateRecipeDraft(draft);
|
|
274
|
+
if (validationError) {
|
|
275
|
+
return { ok: false, error: validationError };
|
|
276
|
+
}
|
|
13
277
|
try {
|
|
14
278
|
mkdirSync(recipesDir, { recursive: true });
|
|
279
|
+
// Nest `vars` under `trigger.vars` (validator only reads it there;
|
|
280
|
+
// top-level was the same shape bug PR #259 fixed for the YAML path).
|
|
281
|
+
const baseTrigger = normalizeRecipeDraftTrigger(draft.trigger);
|
|
282
|
+
const trigger = draft.vars && draft.vars.length > 0
|
|
283
|
+
? {
|
|
284
|
+
...baseTrigger,
|
|
285
|
+
vars: draft.vars.map((item) => ({
|
|
286
|
+
...item,
|
|
287
|
+
name: item.name.trim(),
|
|
288
|
+
})),
|
|
289
|
+
}
|
|
290
|
+
: baseTrigger;
|
|
15
291
|
const payload = {
|
|
16
292
|
name: safeName,
|
|
17
293
|
description: draft.description,
|
|
18
|
-
trigger
|
|
294
|
+
trigger,
|
|
19
295
|
steps: draft.steps.map((s) => ({
|
|
20
|
-
id: s.id,
|
|
296
|
+
id: s.id.trim(),
|
|
21
297
|
agent: s.agent,
|
|
22
298
|
prompt: s.prompt,
|
|
23
299
|
})),
|
|
24
|
-
...(draft.vars && draft.vars.length > 0 ? { vars: draft.vars } : {}),
|
|
25
300
|
createdAt: Date.now(),
|
|
26
301
|
};
|
|
302
|
+
// Surface the FIRST error from `validateRecipeDefinition` — earlier
|
|
303
|
+
// versions filtered to only "Unknown template reference" issues,
|
|
304
|
+
// which silently bypassed cron validation, var-name regex, and
|
|
305
|
+
// reserved-name shadowing on this legacy JSON path. Anyone scripting
|
|
306
|
+
// against the bridge's `POST /recipes` endpoint was getting much
|
|
307
|
+
// weaker validation than the dashboard's YAML PUT path.
|
|
308
|
+
const deepValidation = validateRecipeDefinition(payload);
|
|
309
|
+
const deepError = deepValidation.issues.find((issue) => issue.level === "error");
|
|
310
|
+
if (deepError) {
|
|
311
|
+
return { ok: false, error: deepError.message };
|
|
312
|
+
}
|
|
27
313
|
writeFileSync(candidate, JSON.stringify(payload, null, 2), "utf-8");
|
|
28
314
|
return { ok: true, path: candidate };
|
|
29
315
|
}
|
|
@@ -34,6 +320,426 @@ export function saveRecipe(recipesDir, draft) {
|
|
|
34
320
|
};
|
|
35
321
|
}
|
|
36
322
|
}
|
|
323
|
+
function resolveJsonRecipePathByName(recipesDir, safeName) {
|
|
324
|
+
const candidate = path.resolve(recipesDir, `${safeName}.json`);
|
|
325
|
+
const base = path.resolve(recipesDir);
|
|
326
|
+
if (!candidate.startsWith(base + path.sep))
|
|
327
|
+
return null;
|
|
328
|
+
if (existsSync(candidate))
|
|
329
|
+
return candidate;
|
|
330
|
+
try {
|
|
331
|
+
for (const entry of readdirSync(recipesDir)) {
|
|
332
|
+
if (!entry.endsWith(".json") || entry.endsWith(".permissions.json")) {
|
|
333
|
+
continue;
|
|
334
|
+
}
|
|
335
|
+
const entryPath = path.join(recipesDir, entry);
|
|
336
|
+
try {
|
|
337
|
+
const entryRaw = readFileSync(entryPath, "utf-8");
|
|
338
|
+
const entryParsed = JSON.parse(entryRaw);
|
|
339
|
+
if (entryParsed.name?.toLowerCase() !== safeName) {
|
|
340
|
+
continue;
|
|
341
|
+
}
|
|
342
|
+
return entryPath;
|
|
343
|
+
}
|
|
344
|
+
catch {
|
|
345
|
+
// skip malformed candidate
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
catch {
|
|
350
|
+
return null;
|
|
351
|
+
}
|
|
352
|
+
// Also search install dirs from `recipeInstall`. Skips dirs with
|
|
353
|
+
// `.disabled` marker so the manual-fire / orchestrator path can't
|
|
354
|
+
// resolve a recipe the user has explicitly disabled.
|
|
355
|
+
for (const { entrypointPath } of iterateInstallDirs(recipesDir)) {
|
|
356
|
+
if (!entrypointPath.endsWith(".json"))
|
|
357
|
+
continue;
|
|
358
|
+
try {
|
|
359
|
+
const parsed = JSON.parse(readFileSync(entrypointPath, "utf-8"));
|
|
360
|
+
if (parsed.name?.toLowerCase() === safeName) {
|
|
361
|
+
return entrypointPath;
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
catch {
|
|
365
|
+
// skip malformed
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
return null;
|
|
369
|
+
}
|
|
370
|
+
export function loadRecipeContent(recipesDir, name) {
|
|
371
|
+
const safeName = name.toLowerCase();
|
|
372
|
+
if (!RECIPE_NAME_RE.test(safeName))
|
|
373
|
+
return null;
|
|
374
|
+
const yamlPath = findYamlRecipePath(recipesDir, safeName);
|
|
375
|
+
if (yamlPath) {
|
|
376
|
+
try {
|
|
377
|
+
return {
|
|
378
|
+
content: readFileSync(yamlPath, "utf-8"),
|
|
379
|
+
path: yamlPath,
|
|
380
|
+
};
|
|
381
|
+
}
|
|
382
|
+
catch {
|
|
383
|
+
return null;
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
const jsonPath = resolveJsonRecipePathByName(recipesDir, safeName);
|
|
387
|
+
if (!jsonPath) {
|
|
388
|
+
return null;
|
|
389
|
+
}
|
|
390
|
+
try {
|
|
391
|
+
return {
|
|
392
|
+
content: readFileSync(jsonPath, "utf-8"),
|
|
393
|
+
path: jsonPath,
|
|
394
|
+
};
|
|
395
|
+
}
|
|
396
|
+
catch {
|
|
397
|
+
return null;
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
export function saveRecipeContent(recipesDir, name, content) {
|
|
401
|
+
const safeName = name.toLowerCase();
|
|
402
|
+
if (!RECIPE_NAME_RE.test(safeName)) {
|
|
403
|
+
return { ok: false, error: "Invalid recipe name" };
|
|
404
|
+
}
|
|
405
|
+
if (!content.trim()) {
|
|
406
|
+
return { ok: false, error: "Recipe content is required" };
|
|
407
|
+
}
|
|
408
|
+
let parsed;
|
|
409
|
+
try {
|
|
410
|
+
parsed = parseYaml(content);
|
|
411
|
+
}
|
|
412
|
+
catch (err) {
|
|
413
|
+
return {
|
|
414
|
+
ok: false,
|
|
415
|
+
error: err instanceof Error ? err.message : String(err),
|
|
416
|
+
};
|
|
417
|
+
}
|
|
418
|
+
const validation = validateRecipeDefinition(parsed);
|
|
419
|
+
const warnings = validation.issues
|
|
420
|
+
.filter((issue) => issue.level === "warning")
|
|
421
|
+
.map((issue) => issue.message);
|
|
422
|
+
const validationError = validation.issues.find((issue) => issue.level === "error");
|
|
423
|
+
if (validationError) {
|
|
424
|
+
return {
|
|
425
|
+
ok: false,
|
|
426
|
+
error: validationError.message,
|
|
427
|
+
...(warnings.length > 0 ? { warnings } : {}),
|
|
428
|
+
};
|
|
429
|
+
}
|
|
430
|
+
// If the parsed body's `name:` field disagrees with the filename
|
|
431
|
+
// (e.g. caller PUT to /recipes/myrecipe with a body whose `name:` is
|
|
432
|
+
// `MyRecipe`), rewrite it to match. The filename is the source of
|
|
433
|
+
// truth for routing, dashboard list keys, and webhook resolution;
|
|
434
|
+
// body drift just causes silent confusion.
|
|
435
|
+
//
|
|
436
|
+
// Earlier versions used a `^name:\s*.+$/m` text replace, but that:
|
|
437
|
+
// (a) only handled the FIRST top-level `name:` (YAML duplicate keys
|
|
438
|
+
// — which the parser resolves to the LAST — would survive in
|
|
439
|
+
// the file even after rewrite), and
|
|
440
|
+
// (b) didn't recognize quoted forms like `name: "MyRecipe"` cleanly
|
|
441
|
+
// across all whitespace shapes.
|
|
442
|
+
// Parse → mutate → stringify is robust against both: the YAML
|
|
443
|
+
// serializer normalizes the entire document, so any duplicate/quoted
|
|
444
|
+
// name disappears.
|
|
445
|
+
let normalizedContent = content;
|
|
446
|
+
if (parsed &&
|
|
447
|
+
typeof parsed === "object" &&
|
|
448
|
+
!Array.isArray(parsed) &&
|
|
449
|
+
typeof parsed.name === "string" &&
|
|
450
|
+
parsed.name !== safeName) {
|
|
451
|
+
const oldName = parsed.name;
|
|
452
|
+
const recipe = { ...parsed, name: safeName };
|
|
453
|
+
try {
|
|
454
|
+
normalizedContent = stringifyYaml(recipe);
|
|
455
|
+
warnings.push(`Recipe body name "${oldName}" was rewritten to "${safeName}" to match the filename.`);
|
|
456
|
+
}
|
|
457
|
+
catch {
|
|
458
|
+
// Re-stringify failed (e.g. cyclic structure); fall back to the
|
|
459
|
+
// text replace so save still succeeds. Safe because the parse
|
|
460
|
+
// above already validated the document.
|
|
461
|
+
normalizedContent = content.replace(/^name:\s*.+$/m, `name: ${safeName}`);
|
|
462
|
+
warnings.push(`Recipe body name "${oldName}" was rewritten to "${safeName}" to match the filename (text-replace fallback).`);
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
try {
|
|
466
|
+
mkdirSync(recipesDir, { recursive: true });
|
|
467
|
+
const base = path.resolve(recipesDir);
|
|
468
|
+
const candidate = findYamlRecipePath(recipesDir, safeName) ??
|
|
469
|
+
path.resolve(recipesDir, `${safeName}.yaml`);
|
|
470
|
+
if (!candidate.startsWith(base + path.sep)) {
|
|
471
|
+
return { ok: false, error: "Invalid path" };
|
|
472
|
+
}
|
|
473
|
+
writeFileSync(candidate, normalizedContent.endsWith("\n")
|
|
474
|
+
? normalizedContent
|
|
475
|
+
: `${normalizedContent}\n`, "utf-8");
|
|
476
|
+
return {
|
|
477
|
+
ok: true,
|
|
478
|
+
path: candidate,
|
|
479
|
+
...(warnings.length > 0 ? { warnings } : {}),
|
|
480
|
+
};
|
|
481
|
+
}
|
|
482
|
+
catch (err) {
|
|
483
|
+
return {
|
|
484
|
+
ok: false,
|
|
485
|
+
error: err instanceof Error ? err.message : String(err),
|
|
486
|
+
};
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
/**
|
|
490
|
+
* Deletes a recipe file (yaml/yml or json) plus any sidecar permissions file.
|
|
491
|
+
* Returns ok=false with a 404-style error when the recipe cannot be located.
|
|
492
|
+
*/
|
|
493
|
+
export function deleteRecipeContent(recipesDir, name) {
|
|
494
|
+
const safeName = name.toLowerCase();
|
|
495
|
+
if (!RECIPE_NAME_RE.test(safeName)) {
|
|
496
|
+
return { ok: false, error: "Invalid recipe name" };
|
|
497
|
+
}
|
|
498
|
+
const base = path.resolve(recipesDir);
|
|
499
|
+
const target = findYamlRecipePath(recipesDir, safeName) ??
|
|
500
|
+
resolveJsonRecipePathByName(recipesDir, safeName);
|
|
501
|
+
if (!target) {
|
|
502
|
+
return { ok: false, error: "Recipe not found" };
|
|
503
|
+
}
|
|
504
|
+
const resolved = path.resolve(target);
|
|
505
|
+
if (!resolved.startsWith(base + path.sep)) {
|
|
506
|
+
return { ok: false, error: "Invalid path" };
|
|
507
|
+
}
|
|
508
|
+
try {
|
|
509
|
+
rmSync(resolved, { force: true });
|
|
510
|
+
const sidecar = `${resolved}.permissions.json`;
|
|
511
|
+
if (existsSync(sidecar)) {
|
|
512
|
+
try {
|
|
513
|
+
rmSync(sidecar, { force: true });
|
|
514
|
+
}
|
|
515
|
+
catch {
|
|
516
|
+
// sidecar removal best-effort
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
return { ok: true, path: resolved };
|
|
520
|
+
}
|
|
521
|
+
catch (err) {
|
|
522
|
+
return {
|
|
523
|
+
ok: false,
|
|
524
|
+
error: err instanceof Error ? err.message : String(err),
|
|
525
|
+
};
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
/**
|
|
529
|
+
* Duplicate a recipe as a variant. Copies the source YAML, rewrites the
|
|
530
|
+
* `name:` field to `<original>-v<N>` (first available suffix), and writes
|
|
531
|
+
* the copy to disk. Returns the new variant name and path on success.
|
|
532
|
+
*
|
|
533
|
+
* The variant name follows the same validation rules as recipe names.
|
|
534
|
+
* Suffixes v2..v9 are tried before returning an error.
|
|
535
|
+
*/
|
|
536
|
+
export function duplicateRecipe(recipesDir, sourceName) {
|
|
537
|
+
const safeName = sourceName.toLowerCase();
|
|
538
|
+
if (!RECIPE_NAME_RE.test(safeName)) {
|
|
539
|
+
return { ok: false, error: "Invalid recipe name" };
|
|
540
|
+
}
|
|
541
|
+
const source = loadRecipeContent(recipesDir, safeName);
|
|
542
|
+
if (!source) {
|
|
543
|
+
return { ok: false, error: "Recipe not found" };
|
|
544
|
+
}
|
|
545
|
+
if (!/\.ya?ml$/i.test(source.path)) {
|
|
546
|
+
return {
|
|
547
|
+
ok: false,
|
|
548
|
+
error: "Recipe variants are only supported for YAML recipes",
|
|
549
|
+
};
|
|
550
|
+
}
|
|
551
|
+
if (!/^name:\s*.+$/m.test(source.content)) {
|
|
552
|
+
return {
|
|
553
|
+
ok: false,
|
|
554
|
+
error: "Source recipe is missing a top-level 'name:' field",
|
|
555
|
+
};
|
|
556
|
+
}
|
|
557
|
+
// Determine next available variant name: strip any existing -vN suffix,
|
|
558
|
+
// then try -v2 through -v9.
|
|
559
|
+
const base = safeName.replace(/-v\d+$/, "");
|
|
560
|
+
let variantName = null;
|
|
561
|
+
for (let n = 2; n <= 9; n++) {
|
|
562
|
+
const candidate = `${base}-v${n}`;
|
|
563
|
+
if (!findYamlRecipePath(recipesDir, candidate)) {
|
|
564
|
+
variantName = candidate;
|
|
565
|
+
break;
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
if (!variantName) {
|
|
569
|
+
return {
|
|
570
|
+
ok: false,
|
|
571
|
+
error: "Too many variants already exist (v2–v9 taken)",
|
|
572
|
+
};
|
|
573
|
+
}
|
|
574
|
+
// Rewrite the name: field in the YAML. Simple line-by-line replacement
|
|
575
|
+
// is safe here: the name field is always a scalar on its own line.
|
|
576
|
+
const newContent = source.content.replace(/^name:\s*.+$/m, `name: ${variantName}`);
|
|
577
|
+
const saveResult = saveRecipeContent(recipesDir, variantName, newContent);
|
|
578
|
+
if (!saveResult.ok) {
|
|
579
|
+
return { ok: false, error: saveResult.error };
|
|
580
|
+
}
|
|
581
|
+
return { ok: true, variantName, path: saveResult.path };
|
|
582
|
+
}
|
|
583
|
+
/**
|
|
584
|
+
* Promote a variant recipe to become the canonical name.
|
|
585
|
+
*
|
|
586
|
+
* Steps:
|
|
587
|
+
* 1. Load the variant's YAML.
|
|
588
|
+
* 2. Rewrite its `name:` field to `targetName`.
|
|
589
|
+
* 3. Save under `targetName` (overwrites any existing file at that name).
|
|
590
|
+
* 4. Delete the variant file so only one copy exists.
|
|
591
|
+
*
|
|
592
|
+
* The caller supplies `variantName` (e.g. "morning-brief-v2") and
|
|
593
|
+
* `targetName` (e.g. "morning-brief"). Both must pass the recipe name
|
|
594
|
+
* validation regex. Returns `{ ok, path }` on success.
|
|
595
|
+
*/
|
|
596
|
+
export async function promoteRecipeVariant(recipesDir, variantName, targetName, options) {
|
|
597
|
+
const safeVariant = variantName.toLowerCase();
|
|
598
|
+
const safeTarget = targetName.toLowerCase();
|
|
599
|
+
if (!RECIPE_NAME_RE.test(safeVariant) || !RECIPE_NAME_RE.test(safeTarget)) {
|
|
600
|
+
return { ok: false, error: "Invalid recipe name" };
|
|
601
|
+
}
|
|
602
|
+
if (safeVariant === safeTarget) {
|
|
603
|
+
return { ok: false, error: "Variant and target names must differ" };
|
|
604
|
+
}
|
|
605
|
+
const source = loadRecipeContent(recipesDir, safeVariant);
|
|
606
|
+
if (!source) {
|
|
607
|
+
return { ok: false, error: "Variant recipe not found" };
|
|
608
|
+
}
|
|
609
|
+
if (!/\.ya?ml$/i.test(source.path)) {
|
|
610
|
+
return {
|
|
611
|
+
ok: false,
|
|
612
|
+
error: "Recipe variants are only supported for YAML recipes",
|
|
613
|
+
};
|
|
614
|
+
}
|
|
615
|
+
if (!/^name:\s*.+$/m.test(source.content)) {
|
|
616
|
+
return {
|
|
617
|
+
ok: false,
|
|
618
|
+
error: "Variant recipe is missing a top-level 'name:' field",
|
|
619
|
+
};
|
|
620
|
+
}
|
|
621
|
+
// Guard against silent overwrites: if the target already exists the caller
|
|
622
|
+
// must pass force:true. We also capture the prior content hash for audit.
|
|
623
|
+
const existing = loadRecipeContent(recipesDir, safeTarget);
|
|
624
|
+
if (existing && !options?.force) {
|
|
625
|
+
return {
|
|
626
|
+
ok: false,
|
|
627
|
+
targetExists: true,
|
|
628
|
+
error: `Recipe "${safeTarget}" already exists. Pass force:true to overwrite.`,
|
|
629
|
+
};
|
|
630
|
+
}
|
|
631
|
+
// Write audit log entry before the overwrite so the replaced content is
|
|
632
|
+
// traceable even if the variant file is deleted in the next step.
|
|
633
|
+
if (existing) {
|
|
634
|
+
try {
|
|
635
|
+
const priorHash = createHash("sha256")
|
|
636
|
+
.update(existing.content)
|
|
637
|
+
.digest("hex");
|
|
638
|
+
const auditPath = existing.path.replace(/\.ya?ml$/, ".promote-audit.json");
|
|
639
|
+
writeFileSync(auditPath, JSON.stringify({
|
|
640
|
+
ts: new Date().toISOString(),
|
|
641
|
+
action: "promote_overwrite",
|
|
642
|
+
variantName: safeVariant,
|
|
643
|
+
targetName: safeTarget,
|
|
644
|
+
priorContentHash: priorHash,
|
|
645
|
+
priorContentPath: existing.path,
|
|
646
|
+
}, null, 2), "utf-8");
|
|
647
|
+
}
|
|
648
|
+
catch {
|
|
649
|
+
// Audit log failure must not block the promote — log and continue.
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
const newContent = source.content.replace(/^name:\s*.+$/m, `name: ${safeTarget}`);
|
|
653
|
+
const saveResult = saveRecipeContent(recipesDir, safeTarget, newContent);
|
|
654
|
+
if (!saveResult.ok) {
|
|
655
|
+
return { ok: false, error: saveResult.error };
|
|
656
|
+
}
|
|
657
|
+
// Delete the variant file — best-effort; don't fail the promote if cleanup fails.
|
|
658
|
+
deleteRecipeContent(recipesDir, safeVariant);
|
|
659
|
+
return { ok: true, path: saveResult.path };
|
|
660
|
+
}
|
|
661
|
+
/**
|
|
662
|
+
* Lints raw YAML/JSON recipe content without writing to disk. Used by the
|
|
663
|
+
* dashboard edit UI to surface validateRecipeDefinition warnings live, in
|
|
664
|
+
* addition to the warnings returned by saveRecipeContent on save.
|
|
665
|
+
*/
|
|
666
|
+
export function lintRecipeContent(content) {
|
|
667
|
+
if (!content.trim()) {
|
|
668
|
+
return { ok: false, errors: ["Recipe content is required"], warnings: [] };
|
|
669
|
+
}
|
|
670
|
+
let parsed;
|
|
671
|
+
try {
|
|
672
|
+
parsed = parseYaml(content);
|
|
673
|
+
}
|
|
674
|
+
catch (err) {
|
|
675
|
+
return {
|
|
676
|
+
ok: false,
|
|
677
|
+
errors: [err instanceof Error ? err.message : String(err)],
|
|
678
|
+
warnings: [],
|
|
679
|
+
};
|
|
680
|
+
}
|
|
681
|
+
const validation = validateRecipeDefinition(parsed);
|
|
682
|
+
const errors = [];
|
|
683
|
+
const warnings = [];
|
|
684
|
+
for (const issue of validation.issues) {
|
|
685
|
+
if (issue.level === "error")
|
|
686
|
+
errors.push(issue.message);
|
|
687
|
+
else
|
|
688
|
+
warnings.push(issue.message);
|
|
689
|
+
}
|
|
690
|
+
return { ok: errors.length === 0, errors, warnings };
|
|
691
|
+
}
|
|
692
|
+
// ---------------------------------------------------------------------------
|
|
693
|
+
// Recipe trust levels
|
|
694
|
+
// ---------------------------------------------------------------------------
|
|
695
|
+
export const TRUST_LEVELS = [
|
|
696
|
+
"draft",
|
|
697
|
+
"manual_run",
|
|
698
|
+
"ask_every_time",
|
|
699
|
+
"ask_novel",
|
|
700
|
+
"mostly_trusted",
|
|
701
|
+
"fully_trusted",
|
|
702
|
+
];
|
|
703
|
+
const TRUST_LEVELS_FILE = "trust_levels.json";
|
|
704
|
+
function trustLevelsPath(recipesDir) {
|
|
705
|
+
return path.join(recipesDir, TRUST_LEVELS_FILE);
|
|
706
|
+
}
|
|
707
|
+
function loadTrustLevels(recipesDir) {
|
|
708
|
+
const p = trustLevelsPath(recipesDir);
|
|
709
|
+
try {
|
|
710
|
+
const raw = readFileSync(p, "utf-8");
|
|
711
|
+
return JSON.parse(raw);
|
|
712
|
+
}
|
|
713
|
+
catch {
|
|
714
|
+
return {};
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
function saveTrustLevels(recipesDir, levels) {
|
|
718
|
+
const p = trustLevelsPath(recipesDir);
|
|
719
|
+
mkdirSync(recipesDir, { recursive: true });
|
|
720
|
+
writeFileSync(p, JSON.stringify(levels, null, 2), "utf-8");
|
|
721
|
+
}
|
|
722
|
+
export function getTrustLevel(recipesDir, name) {
|
|
723
|
+
const levels = loadTrustLevels(recipesDir);
|
|
724
|
+
return levels[name] ?? "draft";
|
|
725
|
+
}
|
|
726
|
+
export function setTrustLevel(recipesDir, name, level) {
|
|
727
|
+
if (!TRUST_LEVELS.includes(level)) {
|
|
728
|
+
return { ok: false, error: `Invalid trust level: ${level}` };
|
|
729
|
+
}
|
|
730
|
+
try {
|
|
731
|
+
const levels = loadTrustLevels(recipesDir);
|
|
732
|
+
levels[name] = level;
|
|
733
|
+
saveTrustLevels(recipesDir, levels);
|
|
734
|
+
return { ok: true };
|
|
735
|
+
}
|
|
736
|
+
catch (err) {
|
|
737
|
+
return {
|
|
738
|
+
ok: false,
|
|
739
|
+
error: err instanceof Error ? err.message : String(err),
|
|
740
|
+
};
|
|
741
|
+
}
|
|
742
|
+
}
|
|
37
743
|
export function listInstalledRecipes(recipesDir) {
|
|
38
744
|
let entries;
|
|
39
745
|
try {
|
|
@@ -42,24 +748,20 @@ export function listInstalledRecipes(recipesDir) {
|
|
|
42
748
|
catch {
|
|
43
749
|
return { recipesDir, recipes: [] };
|
|
44
750
|
}
|
|
751
|
+
const cfg = loadConfig();
|
|
752
|
+
const disabledSet = new Set(cfg.recipes?.disabled ?? []);
|
|
753
|
+
const trustLevels = loadTrustLevels(recipesDir);
|
|
45
754
|
const recipes = [];
|
|
46
755
|
for (const f of entries) {
|
|
47
|
-
|
|
756
|
+
const isYaml = f.endsWith(".yaml") || f.endsWith(".yml");
|
|
757
|
+
const isJson = f.endsWith(".json") && !f.endsWith(".permissions.json");
|
|
758
|
+
if (!isYaml && !isJson)
|
|
48
759
|
continue;
|
|
49
760
|
const fullPath = path.join(recipesDir, f);
|
|
50
761
|
try {
|
|
51
762
|
const raw = readFileSync(fullPath, "utf-8");
|
|
52
|
-
const parsed = JSON.parse(raw);
|
|
763
|
+
const parsed = (isYaml ? parseYaml(raw) : JSON.parse(raw));
|
|
53
764
|
const stat = statSync(fullPath);
|
|
54
|
-
const permsPath = `${fullPath}.permissions.json`;
|
|
55
|
-
let hasPermissions = false;
|
|
56
|
-
try {
|
|
57
|
-
statSync(permsPath);
|
|
58
|
-
hasPermissions = true;
|
|
59
|
-
}
|
|
60
|
-
catch {
|
|
61
|
-
// no permissions sidecar
|
|
62
|
-
}
|
|
63
765
|
const resolvedRecipesDir = path.resolve(recipesDir);
|
|
64
766
|
let source;
|
|
65
767
|
if (fullPath.startsWith(resolvedRecipesDir + path.sep) ||
|
|
@@ -72,27 +774,205 @@ export function listInstalledRecipes(recipesDir) {
|
|
|
72
774
|
else {
|
|
73
775
|
source = "unknown";
|
|
74
776
|
}
|
|
777
|
+
const ext = isYaml ? (f.endsWith(".yml") ? ".yml" : ".yaml") : ".json";
|
|
778
|
+
const parsedName = parsed.name ?? path.basename(f, ext);
|
|
779
|
+
const lintRes = validateRecipeDefinition(parsed);
|
|
780
|
+
let errCount = 0;
|
|
781
|
+
let warnCount = 0;
|
|
782
|
+
let firstError;
|
|
783
|
+
for (const issue of lintRes.issues) {
|
|
784
|
+
if (issue.level === "error") {
|
|
785
|
+
errCount++;
|
|
786
|
+
if (!firstError)
|
|
787
|
+
firstError = issue.message;
|
|
788
|
+
}
|
|
789
|
+
else {
|
|
790
|
+
warnCount++;
|
|
791
|
+
}
|
|
792
|
+
}
|
|
793
|
+
const webhookPath = parsed.trigger?.type === "webhook" &&
|
|
794
|
+
typeof parsed.trigger?.path === "string"
|
|
795
|
+
? parsed.trigger.path
|
|
796
|
+
: undefined;
|
|
75
797
|
recipes.push({
|
|
76
|
-
name:
|
|
798
|
+
name: parsedName,
|
|
77
799
|
description: parsed.description,
|
|
78
800
|
trigger: parsed.trigger?.type,
|
|
801
|
+
...(webhookPath ? { webhookPath } : {}),
|
|
79
802
|
stepCount: Array.isArray(parsed.steps) ? parsed.steps.length : 0,
|
|
80
803
|
path: fullPath,
|
|
81
804
|
installedAt: stat.mtimeMs,
|
|
82
|
-
hasPermissions,
|
|
83
805
|
source,
|
|
806
|
+
// Top-level legacy recipes don't have install dirs to put a marker
|
|
807
|
+
// in, so the `enabled` field still comes from the legacy config list.
|
|
808
|
+
enabled: !disabledSet.has(parsedName),
|
|
809
|
+
trustLevel: (trustLevels[parsedName] ?? "draft"),
|
|
84
810
|
...(Array.isArray(parsed.vars) && parsed.vars.length > 0
|
|
85
811
|
? { vars: parsed.vars }
|
|
86
812
|
: {}),
|
|
813
|
+
lint: {
|
|
814
|
+
ok: errCount === 0,
|
|
815
|
+
errorCount: errCount,
|
|
816
|
+
warningCount: warnCount,
|
|
817
|
+
...(firstError ? { firstError } : {}),
|
|
818
|
+
},
|
|
87
819
|
});
|
|
88
820
|
}
|
|
89
821
|
catch {
|
|
90
822
|
// skip malformed recipe file
|
|
91
823
|
}
|
|
92
824
|
}
|
|
825
|
+
// Second pass — recipes installed via `runRecipeInstall` into subdirs.
|
|
826
|
+
// `enabled` reflects the per-install `.disabled` marker; the legacy
|
|
827
|
+
// config disabled list is a top-level concern (we still apply it as a
|
|
828
|
+
// safety belt in case a name collides).
|
|
829
|
+
for (const { installDir, entrypointPath, enabled: installEnabled, } of iterateInstallDirs(recipesDir, { includeDisabled: true })) {
|
|
830
|
+
try {
|
|
831
|
+
const ext = path.extname(entrypointPath).toLowerCase();
|
|
832
|
+
const isYaml = ext === ".yaml" || ext === ".yml";
|
|
833
|
+
const isJson = ext === ".json";
|
|
834
|
+
if (!isYaml && !isJson)
|
|
835
|
+
continue;
|
|
836
|
+
const raw = readFileSync(entrypointPath, "utf-8");
|
|
837
|
+
const parsed = (isYaml ? parseYaml(raw) : JSON.parse(raw));
|
|
838
|
+
const stat = statSync(entrypointPath);
|
|
839
|
+
const parsedName = parsed.name ??
|
|
840
|
+
path.basename(entrypointPath, path.extname(entrypointPath));
|
|
841
|
+
const lintRes = validateRecipeDefinition(parsed);
|
|
842
|
+
let errCount = 0;
|
|
843
|
+
let warnCount = 0;
|
|
844
|
+
let firstError;
|
|
845
|
+
for (const issue of lintRes.issues) {
|
|
846
|
+
if (issue.level === "error") {
|
|
847
|
+
errCount++;
|
|
848
|
+
if (!firstError)
|
|
849
|
+
firstError = issue.message;
|
|
850
|
+
}
|
|
851
|
+
else {
|
|
852
|
+
warnCount++;
|
|
853
|
+
}
|
|
854
|
+
}
|
|
855
|
+
const webhookPath = parsed.trigger?.type === "webhook" &&
|
|
856
|
+
typeof parsed.trigger?.path === "string"
|
|
857
|
+
? parsed.trigger.path
|
|
858
|
+
: undefined;
|
|
859
|
+
recipes.push({
|
|
860
|
+
name: parsedName,
|
|
861
|
+
description: parsed.description,
|
|
862
|
+
trigger: parsed.trigger?.type,
|
|
863
|
+
...(webhookPath ? { webhookPath } : {}),
|
|
864
|
+
stepCount: Array.isArray(parsed.steps) ? parsed.steps.length : 0,
|
|
865
|
+
path: entrypointPath,
|
|
866
|
+
installedAt: stat.mtimeMs,
|
|
867
|
+
source: "user",
|
|
868
|
+
// Disabled if EITHER the install marker is set OR the legacy config
|
|
869
|
+
// names this recipe — defence-in-depth so a stale config entry can't
|
|
870
|
+
// accidentally re-enable a recipe the user explicitly disabled, and
|
|
871
|
+
// the dashboard can't accidentally enable one disabled by an admin
|
|
872
|
+
// through the legacy file.
|
|
873
|
+
enabled: installEnabled && !disabledSet.has(parsedName),
|
|
874
|
+
trustLevel: (trustLevels[parsedName] ?? "draft"),
|
|
875
|
+
...(Array.isArray(parsed.vars) && parsed.vars.length > 0
|
|
876
|
+
? { vars: parsed.vars }
|
|
877
|
+
: {}),
|
|
878
|
+
lint: {
|
|
879
|
+
ok: errCount === 0,
|
|
880
|
+
errorCount: errCount,
|
|
881
|
+
warningCount: warnCount,
|
|
882
|
+
...(firstError ? { firstError } : {}),
|
|
883
|
+
},
|
|
884
|
+
});
|
|
885
|
+
void installDir;
|
|
886
|
+
}
|
|
887
|
+
catch {
|
|
888
|
+
// skip malformed install dir
|
|
889
|
+
}
|
|
890
|
+
}
|
|
93
891
|
recipes.sort((a, b) => a.name.localeCompare(b.name));
|
|
94
892
|
return { recipesDir, recipes };
|
|
95
893
|
}
|
|
894
|
+
/**
|
|
895
|
+
* Thrown by `findYamlRecipePath` (and listing/lint paths that surface this)
|
|
896
|
+
* when more than one enabled YAML recipe declares the same `name`. Callers
|
|
897
|
+
* must surface this loudly rather than silently picking the first match —
|
|
898
|
+
* dashboard run buttons, scheduler fires, and webhook resolution would all
|
|
899
|
+
* be ambiguous otherwise.
|
|
900
|
+
*/
|
|
901
|
+
export class RecipeNameConflictError extends Error {
|
|
902
|
+
recipeName;
|
|
903
|
+
paths;
|
|
904
|
+
constructor(recipeName, paths) {
|
|
905
|
+
super(`Multiple YAML recipes declare name "${recipeName}": ${paths
|
|
906
|
+
.map((p) => path.basename(p))
|
|
907
|
+
.join(", ")}`);
|
|
908
|
+
this.name = "RecipeNameConflictError";
|
|
909
|
+
this.recipeName = recipeName;
|
|
910
|
+
this.paths = paths;
|
|
911
|
+
}
|
|
912
|
+
}
|
|
913
|
+
export function findYamlRecipePath(recipesDir, name) {
|
|
914
|
+
const safeName = name.toLowerCase();
|
|
915
|
+
if (!RECIPE_NAME_RE.test(safeName))
|
|
916
|
+
return null;
|
|
917
|
+
const base = path.resolve(recipesDir);
|
|
918
|
+
const matches = new Set();
|
|
919
|
+
// Exact-filename matches (top-level legacy layout). The parsed `name`
|
|
920
|
+
// field is allowed to differ from the filename, so we still scan below.
|
|
921
|
+
for (const ext of [".yaml", ".yml"]) {
|
|
922
|
+
const candidate = path.resolve(recipesDir, `${safeName}${ext}`);
|
|
923
|
+
if (!candidate.startsWith(base + path.sep))
|
|
924
|
+
return null;
|
|
925
|
+
if (existsSync(candidate))
|
|
926
|
+
matches.add(candidate);
|
|
927
|
+
}
|
|
928
|
+
let entries = [];
|
|
929
|
+
try {
|
|
930
|
+
entries = readdirSync(recipesDir);
|
|
931
|
+
}
|
|
932
|
+
catch {
|
|
933
|
+
// recipesDir missing — fall through; matches may still be empty
|
|
934
|
+
}
|
|
935
|
+
for (const entry of entries) {
|
|
936
|
+
if (!entry.endsWith(".yaml") && !entry.endsWith(".yml"))
|
|
937
|
+
continue;
|
|
938
|
+
const entryPath = path.join(recipesDir, entry);
|
|
939
|
+
if (matches.has(entryPath))
|
|
940
|
+
continue;
|
|
941
|
+
try {
|
|
942
|
+
const entryParsed = parseYaml(readFileSync(entryPath, "utf-8"));
|
|
943
|
+
if (entryParsed.name?.toLowerCase() === safeName) {
|
|
944
|
+
matches.add(entryPath);
|
|
945
|
+
}
|
|
946
|
+
}
|
|
947
|
+
catch {
|
|
948
|
+
// skip malformed candidate
|
|
949
|
+
}
|
|
950
|
+
}
|
|
951
|
+
// Install dirs from `recipeInstall`. iterateInstallDirs skips dirs with
|
|
952
|
+
// `.disabled` marker so the manual-fire / orchestrator path can't
|
|
953
|
+
// resolve a recipe the user has explicitly disabled.
|
|
954
|
+
for (const { entrypointPath } of iterateInstallDirs(recipesDir)) {
|
|
955
|
+
if (!/\.ya?ml$/i.test(entrypointPath))
|
|
956
|
+
continue;
|
|
957
|
+
if (matches.has(entrypointPath))
|
|
958
|
+
continue;
|
|
959
|
+
try {
|
|
960
|
+
const parsed = parseYaml(readFileSync(entrypointPath, "utf-8"));
|
|
961
|
+
if (parsed.name?.toLowerCase() === safeName) {
|
|
962
|
+
matches.add(entrypointPath);
|
|
963
|
+
}
|
|
964
|
+
}
|
|
965
|
+
catch {
|
|
966
|
+
// skip malformed
|
|
967
|
+
}
|
|
968
|
+
}
|
|
969
|
+
if (matches.size === 0)
|
|
970
|
+
return null;
|
|
971
|
+
if (matches.size > 1) {
|
|
972
|
+
throw new RecipeNameConflictError(safeName, [...matches].sort());
|
|
973
|
+
}
|
|
974
|
+
return [...matches][0] ?? null;
|
|
975
|
+
}
|
|
96
976
|
/**
|
|
97
977
|
* Scan recipes and return the first webhook-triggered recipe whose
|
|
98
978
|
* trigger.path matches the requested path. Returns null on miss.
|
|
@@ -106,18 +986,50 @@ export function findWebhookRecipe(recipesDir, requestPath) {
|
|
|
106
986
|
catch {
|
|
107
987
|
return null;
|
|
108
988
|
}
|
|
989
|
+
// Pass 1 — top-level files (legacy)
|
|
109
990
|
for (const f of entries) {
|
|
110
|
-
|
|
991
|
+
const isYaml = f.endsWith(".yaml") || f.endsWith(".yml");
|
|
992
|
+
const isJson = f.endsWith(".json") && !f.endsWith(".permissions.json");
|
|
993
|
+
if (!isYaml && !isJson)
|
|
111
994
|
continue;
|
|
112
995
|
try {
|
|
113
|
-
const
|
|
114
|
-
const
|
|
996
|
+
const filePath = path.join(recipesDir, f);
|
|
997
|
+
const raw = readFileSync(filePath, "utf-8");
|
|
998
|
+
const parsed = (isYaml ? parseYaml(raw) : JSON.parse(raw));
|
|
115
999
|
if (parsed.trigger?.type !== "webhook")
|
|
116
1000
|
continue;
|
|
117
1001
|
if (parsed.trigger.path === requestPath) {
|
|
118
1002
|
return {
|
|
119
|
-
name: parsed.name ?? path.basename(f,
|
|
1003
|
+
name: parsed.name ?? path.basename(f, path.extname(f)),
|
|
120
1004
|
path: requestPath,
|
|
1005
|
+
filePath,
|
|
1006
|
+
format: isYaml ? "yaml" : "json",
|
|
1007
|
+
};
|
|
1008
|
+
}
|
|
1009
|
+
}
|
|
1010
|
+
catch {
|
|
1011
|
+
// skip malformed
|
|
1012
|
+
}
|
|
1013
|
+
}
|
|
1014
|
+
// Pass 2 — install dirs (skips dirs marked .disabled).
|
|
1015
|
+
for (const { entrypointPath } of iterateInstallDirs(recipesDir)) {
|
|
1016
|
+
const ext = path.extname(entrypointPath).toLowerCase();
|
|
1017
|
+
const isYaml = ext === ".yaml" || ext === ".yml";
|
|
1018
|
+
const isJson = ext === ".json";
|
|
1019
|
+
if (!isYaml && !isJson)
|
|
1020
|
+
continue;
|
|
1021
|
+
try {
|
|
1022
|
+
const raw = readFileSync(entrypointPath, "utf-8");
|
|
1023
|
+
const parsed = (isYaml ? parseYaml(raw) : JSON.parse(raw));
|
|
1024
|
+
if (parsed.trigger?.type !== "webhook")
|
|
1025
|
+
continue;
|
|
1026
|
+
if (parsed.trigger.path === requestPath) {
|
|
1027
|
+
return {
|
|
1028
|
+
name: parsed.name ??
|
|
1029
|
+
path.basename(entrypointPath, path.extname(entrypointPath)),
|
|
1030
|
+
path: requestPath,
|
|
1031
|
+
filePath: entrypointPath,
|
|
1032
|
+
format: isYaml ? "yaml" : "json",
|
|
121
1033
|
};
|
|
122
1034
|
}
|
|
123
1035
|
}
|
|
@@ -134,15 +1046,15 @@ export function findWebhookRecipe(recipesDir, requestPath) {
|
|
|
134
1046
|
*/
|
|
135
1047
|
export function loadRecipePrompt(recipesDir, name) {
|
|
136
1048
|
const safeName = name.toLowerCase();
|
|
137
|
-
if (
|
|
1049
|
+
if (!RECIPE_NAME_RE.test(safeName))
|
|
138
1050
|
return null;
|
|
139
|
-
const
|
|
140
|
-
|
|
141
|
-
if (!candidate.startsWith(base + path.sep))
|
|
1051
|
+
const recipePath = resolveJsonRecipePathByName(recipesDir, safeName);
|
|
1052
|
+
if (!recipePath) {
|
|
142
1053
|
return null;
|
|
1054
|
+
}
|
|
143
1055
|
let raw;
|
|
144
1056
|
try {
|
|
145
|
-
raw = readFileSync(
|
|
1057
|
+
raw = readFileSync(recipePath, "utf-8");
|
|
146
1058
|
}
|
|
147
1059
|
catch {
|
|
148
1060
|
return null;
|
|
@@ -166,7 +1078,7 @@ export function loadRecipePrompt(recipesDir, name) {
|
|
|
166
1078
|
}
|
|
167
1079
|
}
|
|
168
1080
|
lines.push("\nWhen finished, print a one-line summary prefixed with 'RECIPE DONE:'.");
|
|
169
|
-
return { prompt: lines.join("\n"), path:
|
|
1081
|
+
return { prompt: lines.join("\n"), path: recipePath };
|
|
170
1082
|
}
|
|
171
1083
|
/**
|
|
172
1084
|
* Append a webhook payload to a base prompt so the agent can reference
|