toolroll 0.9.54 → 0.9.56
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/CHANGELOG.md +19 -0
- package/README.md +29 -12
- package/dist/action-ledger.d.ts +2 -4
- package/dist/action-ledger.js +2 -10
- package/dist/agent-onboard.js +4 -1
- package/dist/approval-policy.d.ts +5 -5
- package/dist/approval-policy.js +4 -4
- package/dist/approval-rules-ui.d.ts +3 -2
- package/dist/approval-rules-ui.js +17 -22
- package/dist/assignment-ui.d.ts +3 -2
- package/dist/assignment-ui.js +4 -11
- package/dist/backup-ui.d.ts +2 -1
- package/dist/backup-ui.js +16 -27
- package/dist/brand-mark.d.ts +3 -2
- package/dist/brand-mark.js +4 -4
- package/dist/browser/workspace.css +1 -1
- package/dist/browser/workspace.js +25 -25
- package/dist/browser-shell.d.ts +2 -1
- package/dist/browser-shell.js +9 -9
- package/dist/browser-workspace.d.ts +14 -15
- package/dist/browser-workspace.js +0 -3
- package/dist/budget-alerts.js +3 -3
- package/dist/chat-actions.d.ts +23 -18
- package/dist/chat-actions.js +110 -88
- package/dist/chat-ask.d.ts +2 -2
- package/dist/chat-ask.js +5 -5
- package/dist/chat-channel.d.ts +15 -15
- package/dist/chat-channel.js +20 -20
- package/dist/chat-controls.d.ts +0 -4
- package/dist/chat-controls.js +0 -1
- package/dist/chat-core.js +1 -1
- package/dist/chat-delivery-state.js +2 -2
- package/dist/chat-delivery.d.ts +2 -2
- package/dist/chat-delivery.js +27 -34
- package/dist/chat-flow.js +3 -3
- package/dist/chat-messages.d.ts +1 -1
- package/dist/chat-messages.js +4 -4
- package/dist/chat-migration.d.ts +1 -1
- package/dist/chat-migration.js +11 -11
- package/dist/chat-polish.d.ts +5 -4
- package/dist/chat-polish.js +9 -10
- package/dist/chat-rooms.js +3 -3
- package/dist/chat-warmth.d.ts +2 -2
- package/dist/check-levels-ui.d.ts +2 -1
- package/dist/check-levels-ui.js +26 -53
- package/dist/cli.d.ts +3 -1
- package/dist/cli.js +15 -10
- package/dist/coding-shipping-ui.d.ts +2 -1
- package/dist/coding-shipping-ui.js +6 -6
- package/dist/coding-ui.d.ts +3 -1
- package/dist/coding-ui.js +30 -29
- package/dist/contracts/chat-actions.d.ts +32 -27
- package/dist/contracts/chat-actions.js +29 -11
- package/dist/contracts/chat-content.d.ts +1 -1
- package/dist/contracts/chat-content.js +1 -1
- package/dist/contracts/chat-tables.js +4 -4
- package/dist/contracts/cli.d.ts +1 -3
- package/dist/contracts/console-api.d.ts +7 -7
- package/dist/contracts/console-api.js +13 -12
- package/dist/contracts/flow-propose.d.ts +3 -3
- package/dist/contracts/flow.d.ts +132 -125
- package/dist/contracts/flow.js +27 -12
- package/dist/contracts/health.d.ts +1 -3
- package/dist/contracts/lead-context.d.ts +1 -1
- package/dist/contracts/lead-context.js +2 -2
- package/dist/contracts/lead-tools.d.ts +20 -18
- package/dist/contracts/lead-tools.js +15 -15
- package/dist/contracts/stage-output.d.ts +1 -1
- package/dist/contracts/stage-output.js +1 -1
- package/dist/contracts/store-json.d.ts +23 -22
- package/dist/contracts/store-json.js +17 -17
- package/dist/contracts/{teammate-turn.d.ts → subagent-turn.d.ts} +5 -5
- package/dist/contracts/{teammate-turn.js → subagent-turn.js} +7 -7
- package/dist/control-ui.d.ts +5 -4
- package/dist/control-ui.js +9 -34
- package/dist/converse.d.ts +23 -23
- package/dist/converse.js +28 -28
- package/dist/coordinator-proposals.d.ts +2 -2
- package/dist/coordinator-proposals.js +1 -1
- package/dist/coordinator.js +1 -1
- package/dist/credentials-ui.d.ts +5 -4
- package/dist/credentials-ui.js +23 -24
- package/dist/demo-chat.d.ts +3 -3
- package/dist/demo-chat.js +29 -76
- package/dist/deprecations.d.ts +12 -0
- package/dist/deprecations.js +20 -0
- package/dist/desktop-update-gate.js +8 -4
- package/dist/desktop-update.js +6 -1
- package/dist/discord-chat.js +6 -6
- package/dist/discord-settings.d.ts +3 -2
- package/dist/discord-settings.js +17 -42
- package/dist/dispatch.d.ts +1 -1
- package/dist/dispatch.js +1 -1
- package/dist/envelope.js +10 -1
- package/dist/evidence-pack.d.ts +2 -1
- package/dist/evidence-pack.js +45 -49
- package/dist/export-ui.d.ts +7 -2
- package/dist/export-ui.js +4 -10
- package/dist/export.js +7 -7
- package/dist/flow-engine.d.ts +2 -2
- package/dist/flow-engine.js +21 -21
- package/dist/flow-gallery-ui.d.ts +4 -3
- package/dist/flow-gallery-ui.js +28 -43
- package/dist/flow-insights.d.ts +1 -1
- package/dist/flow-share.js +13 -8
- package/dist/flow-starters-ui.d.ts +2 -1
- package/dist/flow-starters-ui.js +12 -15
- package/dist/flow-steps.d.ts +5 -5
- package/dist/flow-steps.js +19 -19
- package/dist/flow-triggers.d.ts +2 -1
- package/dist/flow-triggers.js +17 -15
- package/dist/flows-cli.js +1 -1
- package/dist/flows-ui.d.ts +4 -3
- package/dist/flows-ui.js +38 -49
- package/dist/flows.js +21 -20
- package/dist/guides.js +112 -6
- package/dist/html.d.ts +76 -0
- package/dist/html.js +173 -0
- package/dist/integrations-ui.d.ts +2 -1
- package/dist/integrations-ui.js +19 -23
- package/dist/invoke.js +1 -1
- package/dist/kits-ui.d.ts +5 -4
- package/dist/kits-ui.js +22 -24
- package/dist/kits.d.ts +12 -12
- package/dist/kits.js +31 -31
- package/dist/knowledge-ui.d.ts +6 -5
- package/dist/knowledge-ui.js +26 -25
- package/dist/{mate-cli.d.ts → lead-cli.d.ts} +11 -11
- package/dist/{mate-cli.js → lead-cli.js} +38 -38
- package/dist/lead-commitments.js +4 -4
- package/dist/lead-context.d.ts +12 -2
- package/dist/lead-context.js +15 -17
- package/dist/{mate-contract.d.ts → lead-contract.d.ts} +3 -3
- package/dist/{mate-contract.js → lead-contract.js} +7 -6
- package/dist/{mate-doors.d.ts → lead-doors.d.ts} +6 -6
- package/dist/{mate-doors.js → lead-doors.js} +24 -24
- package/dist/lead-follow.d.ts +5 -5
- package/dist/lead-follow.js +9 -9
- package/dist/lead-people.d.ts +5 -5
- package/dist/lead-people.js +14 -14
- package/dist/{mate-progress.d.ts → lead-progress.d.ts} +9 -9
- package/dist/{mate-progress.js → lead-progress.js} +3 -3
- package/dist/{mate-tools.d.ts → lead-tools.d.ts} +19 -19
- package/dist/{mate-tools.js → lead-tools.js} +109 -101
- package/dist/{mate.d.ts → lead.d.ts} +27 -27
- package/dist/{mate.js → lead.js} +102 -102
- package/dist/ledger-view.d.ts +2 -1
- package/dist/ledger-view.js +27 -29
- package/dist/limits-ui.d.ts +3 -2
- package/dist/limits-ui.js +8 -8
- package/dist/live-bus.d.ts +50 -8
- package/dist/live-bus.js +133 -50
- package/dist/mcp-connect.d.ts +1 -1
- package/dist/mcp-connect.js +3 -3
- package/dist/mcp-core.js +1 -1
- package/dist/mcp-oauth.js +35 -36
- package/dist/memory-pass.d.ts +2 -2
- package/dist/memory-pass.js +7 -7
- package/dist/models-ui.d.ts +2 -1
- package/dist/models-ui.js +25 -35
- package/dist/monitoring-ui.d.ts +4 -3
- package/dist/monitoring-ui.js +13 -26
- package/dist/notification-rows.d.ts +2 -2
- package/dist/openrouter-models.d.ts +2 -1
- package/dist/openrouter-models.js +5 -9
- package/dist/operate.d.ts +3 -3
- package/dist/operate.js +46 -41
- package/dist/people-audit-ui.d.ts +2 -1
- package/dist/people-audit-ui.js +13 -13
- package/dist/policy-ui.d.ts +3 -2
- package/dist/policy-ui.js +17 -23
- package/dist/policy.d.ts +1 -1
- package/dist/principal.d.ts +1 -1
- package/dist/principal.js +2 -2
- package/dist/project-delete-ui.d.ts +4 -3
- package/dist/project-delete-ui.js +16 -28
- package/dist/project-delete.d.ts +1 -1
- package/dist/project-delete.js +26 -26
- package/dist/project-memory.js +1 -1
- package/dist/project-tools.d.ts +1 -1
- package/dist/project-tools.js +1 -1
- package/dist/pull-request-ui.d.ts +2 -1
- package/dist/pull-request-ui.js +17 -32
- package/dist/recipe-ui.d.ts +8 -7
- package/dist/recipe-ui.js +39 -32
- package/dist/release-adapters.d.ts +60 -0
- package/dist/release-adapters.js +435 -0
- package/dist/release-cli.d.ts +24 -0
- package/dist/release-cli.js +181 -0
- package/dist/release-gate.d.ts +80 -0
- package/dist/release-gate.js +150 -0
- package/dist/release-links.d.ts +29 -0
- package/dist/release-links.js +102 -0
- package/dist/release.d.ts +331 -0
- package/dist/release.js +550 -0
- package/dist/reply-shape.d.ts +2 -1
- package/dist/reply-shape.js +5 -5
- package/dist/repository-context-ui.d.ts +2 -1
- package/dist/repository-context-ui.js +2 -7
- package/dist/request-budget-ui.d.ts +2 -1
- package/dist/request-budget-ui.js +16 -16
- package/dist/result-review.d.ts +2 -3
- package/dist/result-review.js +3 -9
- package/dist/retention-ui.d.ts +7 -1
- package/dist/retention-ui.js +7 -11
- package/dist/retention.js +4 -4
- package/dist/scope.d.ts +2 -2
- package/dist/scout-report.d.ts +1 -1
- package/dist/scout-report.js +1 -1
- package/dist/serve.d.ts +11 -2
- package/dist/serve.js +105 -99
- package/dist/server/chat.d.ts +7 -10
- package/dist/server/chat.js +984 -1021
- package/dist/server/chrome.d.ts +291 -0
- package/dist/server/chrome.js +747 -0
- package/dist/server/console.css +2819 -0
- package/dist/server/flows.d.ts +2 -6
- package/dist/server/flows.js +854 -773
- package/dist/server/guards.d.ts +1 -1
- package/dist/server/guards.js +8 -5
- package/dist/server/handler-context.d.ts +1 -1
- package/dist/server/handler-context.js +1 -1
- package/dist/server/handler-registry.d.ts +8 -6
- package/dist/server/handler-registry.js +14 -2
- package/dist/server/http.d.ts +274 -0
- package/dist/server/http.js +239 -0
- package/dist/server/live.d.ts +34 -0
- package/dist/server/live.js +309 -0
- package/dist/server/pages.d.ts +2 -6
- package/dist/server/pages.js +808 -789
- package/dist/server/people-tokens.d.ts +1 -6
- package/dist/server/people-tokens.js +581 -573
- package/dist/server/remote.d.ts +4 -7
- package/dist/server/remote.js +39 -70
- package/dist/server/render-chat.d.ts +402 -0
- package/dist/server/render-chat.js +927 -0
- package/dist/server/render-pages.d.ts +535 -0
- package/dist/server/render-pages.js +1277 -0
- package/dist/server/render-people.d.ts +32 -0
- package/dist/server/render-people.js +121 -0
- package/dist/server/render-results.d.ts +744 -0
- package/dist/server/render-results.js +2237 -0
- package/dist/server/render-settings.d.ts +154 -0
- package/dist/server/render-settings.js +580 -0
- package/dist/server/render-tasks.d.ts +632 -0
- package/dist/server/render-tasks.js +2345 -0
- package/dist/server/request-context.d.ts +46 -0
- package/dist/server/request-context.js +17 -0
- package/dist/server/route-table.d.ts +25 -3
- package/dist/server/route-table.js +167 -77
- package/dist/server/runtime.d.ts +28 -20
- package/dist/server/runtime.js +7 -1
- package/dist/server/session.d.ts +138 -0
- package/dist/server/session.js +94 -0
- package/dist/server/settings.d.ts +1 -5
- package/dist/server/settings.js +2186 -2121
- package/dist/server/task-views.d.ts +11 -4
- package/dist/server/task-views.js +26 -22
- package/dist/server/tasks.d.ts +9 -9
- package/dist/server/tasks.js +1489 -1420
- package/dist/server-telemetry.d.ts +4 -3
- package/dist/server-telemetry.js +4 -8
- package/dist/setup-guide.d.ts +2 -2
- package/dist/skills-ui.d.ts +4 -3
- package/dist/skills-ui.js +14 -17
- package/dist/skills.d.ts +1 -1
- package/dist/skills.js +2 -1
- package/dist/slack-chat.d.ts +2 -2
- package/dist/slack-chat.js +6 -6
- package/dist/slack-settings.d.ts +3 -2
- package/dist/slack-settings.js +17 -42
- package/dist/spend-ui.d.ts +5 -4
- package/dist/spend-ui.js +17 -40
- package/dist/spend.d.ts +14 -14
- package/dist/spend.js +31 -31
- package/dist/sso-ui.d.ts +3 -2
- package/dist/sso-ui.js +26 -27
- package/dist/storage-ui.d.ts +2 -1
- package/dist/storage-ui.js +14 -33
- package/dist/store-baseline.d.ts +19 -0
- package/dist/store-baseline.js +2873 -0
- package/dist/store.d.ts +197 -249
- package/dist/store.js +747 -3084
- package/dist/subagent-admin.d.ts +53 -0
- package/dist/{teammate-admin.js → subagent-admin.js} +37 -37
- package/dist/{teammate-desk.d.ts → subagent-desk.d.ts} +22 -23
- package/dist/{teammate-desk.js → subagent-desk.js} +26 -35
- package/dist/{teammate-memory.d.ts → subagent-memory.d.ts} +17 -17
- package/dist/{teammate-memory.js → subagent-memory.js} +41 -41
- package/dist/{teammate-question.d.ts → subagent-question.d.ts} +6 -6
- package/dist/{teammate-question.js → subagent-question.js} +17 -17
- package/dist/{teammate-tools.d.ts → subagent-tools.d.ts} +18 -18
- package/dist/{teammate-tools.js → subagent-tools.js} +36 -36
- package/dist/{teammate-week.d.ts → subagent-week.d.ts} +10 -10
- package/dist/{teammate-week.js → subagent-week.js} +23 -23
- package/dist/{teammate-work.d.ts → subagent-work.d.ts} +21 -21
- package/dist/{teammate-work.js → subagent-work.js} +50 -50
- package/dist/subagents-ui.d.ts +22 -0
- package/dist/subagents-ui.js +137 -0
- package/dist/{teammates.d.ts → subagents.d.ts} +11 -11
- package/dist/{teammates.js → subagents.js} +21 -21
- package/dist/subscription-chat.d.ts +13 -13
- package/dist/subscription-chat.js +19 -19
- package/dist/surface.d.ts +19 -2
- package/dist/surface.js +20 -33
- package/dist/task-composer.d.ts +2 -1
- package/dist/task-composer.js +7 -7
- package/dist/task-status.d.ts +4 -3
- package/dist/task-status.js +15 -16
- package/dist/team-cli.d.ts +1 -0
- package/dist/team-cli.js +14 -2
- package/dist/team-http.d.ts +5 -9
- package/dist/team-http.js +5 -63
- package/dist/team-leads.js +13 -13
- package/dist/team-runtime.d.ts +2 -2
- package/dist/team-runtime.js +8 -8
- package/dist/team-ui.d.ts +2 -1
- package/dist/team-ui.js +2 -5
- package/dist/teams-chat.js +6 -6
- package/dist/teams-settings.d.ts +3 -2
- package/dist/teams-settings.js +20 -31
- package/dist/telegram-flow.d.ts +3 -3
- package/dist/telegram-flow.js +3 -3
- package/dist/{telegram-mate.d.ts → telegram-lead.d.ts} +5 -5
- package/dist/{telegram-mate.js → telegram-lead.js} +27 -27
- package/dist/telegram-settings.d.ts +3 -2
- package/dist/telegram-settings.js +14 -25
- package/dist/telegram-status.js +1 -1
- package/dist/telegram-team.d.ts +1 -1
- package/dist/telegram-team.js +5 -5
- package/dist/telegram.d.ts +1 -1
- package/dist/telegram.js +9 -30
- package/dist/text-limits.d.ts +18 -18
- package/dist/text-limits.js +18 -18
- package/dist/toolroll-update-ui.d.ts +3 -2
- package/dist/toolroll-update-ui.js +35 -39
- package/dist/toolroll-update.d.ts +8 -0
- package/dist/toolroll-update.js +45 -8
- package/dist/tools-ui.d.ts +5 -2
- package/dist/tools-ui.js +31 -31
- package/dist/when-html.d.ts +11 -3
- package/dist/when-html.js +5 -6
- package/dist/workspace-ui.d.ts +4 -2
- package/dist/workspace-ui.js +10 -10
- package/package.json +1 -2
- package/dist/server/shared.d.ts +0 -3151
- package/dist/server/shared.js +0 -12001
- package/dist/teammate-admin.d.ts +0 -53
- package/dist/teammates-ui.d.ts +0 -20
- package/dist/teammates-ui.js +0 -166
package/dist/server/flows.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { handlersOf } from './handler-registry.js';
|
|
1
2
|
/** flows handlers, moved without changing their route bodies. */
|
|
2
3
|
import { createHash } from "node:crypto";
|
|
3
4
|
import {} from "node:http";
|
|
@@ -28,806 +29,886 @@ import { authorizedProject, projectName, rowVisible } from "../project.js";
|
|
|
28
29
|
import { recipeAnswersFromForm, recipeDefinitionPreviewHtml, recipeEditorHtml, recipeFromForm, recipeLibraryHtml, recipeRunHtml, recipeScript, workflowPreviewHtml } from "../recipe-ui.js";
|
|
29
30
|
import { createWorkflowPreview, exportRecipe, findRecipe, importRecipe, launchWorkflow, prepareRecipeRun, RecipeError, savedRecipes, saveWorkflowRecipe, starterRecipes, workflowPreview } from "../recipes.js";
|
|
30
31
|
import { isAlive as runnerAlive } from "../runner.js";
|
|
31
|
-
import {
|
|
32
|
-
import { addRoutine, removeRoutine, runRoutine } from "../
|
|
33
|
-
import { editMemory, forgetMemory,
|
|
34
|
-
import { grantTool, revokeTool, rulesFromForm, setToolRules } from "../
|
|
35
|
-
import {
|
|
36
|
-
import {
|
|
37
|
-
import { BLANK_SOUL,
|
|
38
|
-
import {
|
|
32
|
+
import { createSubagentFrom, labelOf, nameOf, saveSoul, sendSubagentSummaries, setSubagentState, subagentSettings } from "../subagent-admin.js";
|
|
33
|
+
import { addRoutine, removeRoutine, runRoutine } from "../subagent-desk.js";
|
|
34
|
+
import { editMemory, forgetMemory, tellSubagent } from "../subagent-memory.js";
|
|
35
|
+
import { grantTool, revokeTool, rulesFromForm, setToolRules } from "../subagent-tools.js";
|
|
36
|
+
import { sendSubagentWeeklies, undoCall } from "../subagent-week.js";
|
|
37
|
+
import { answerSubagentQuestion } from "../subagent-work.js";
|
|
38
|
+
import { BLANK_SOUL, subagentPageHtml, subagentsListHtml } from "../subagents-ui.js";
|
|
39
|
+
import { requestContext } from "./request-context.js";
|
|
40
|
+
import { refuse, screen } from "./chrome.js";
|
|
41
|
+
import { redirect, respond, SAFETY, taskHref } from "./http.js";
|
|
42
|
+
import { html } from "../html.js";
|
|
43
|
+
import {} from "./session.js";
|
|
39
44
|
export function createFlowsHandlers(runtime) {
|
|
40
45
|
const { store, clock, sendScreen, chromeFor, consoleProjects, options, visible, flowRooms, identify, liveCeiling, providerHome, toolHome, restricted, evidenceRoot, authenticateApprover, projectOf, unscopedMode, ceiling, authorizeMutation } = runtime;
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
}
|
|
52
|
-
const
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
const
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
response
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
46
|
+
// ---- reads ---------------------------------------------------------------
|
|
47
|
+
/** A gallery template's page (Flows → New → Use this): its questions, the preview of these answers, and Create. */
|
|
48
|
+
function sendGalleryUse(response, who, template, projects, repo, given, name, notice, said = null) {
|
|
49
|
+
const answers = given ?? galleryDefaults(store, template, repo);
|
|
50
|
+
let preview = null, problem = notice;
|
|
51
|
+
try {
|
|
52
|
+
preview = previewGallery(store, template, repo, answers, who.name, clock());
|
|
53
|
+
}
|
|
54
|
+
catch (error) {
|
|
55
|
+
problem = error instanceof Error ? error.message : "That can't be made here.";
|
|
56
|
+
}
|
|
57
|
+
const diagram = preview?.built.definition ?? galleryDiagram(template);
|
|
58
|
+
const page = galleryUseHtml({ template, projects: projects.map(path => ({ path, name: projectName(path) })), repo, answers, name, preview, problem,
|
|
59
|
+
csrf: who.via === "cookie" ? who.session.csrf : "", diagram, tools: preview?.tools ?? galleryToolsOf(template, diagram, connectionsOf(store, repo)), said });
|
|
60
|
+
return sendScreen(response, problem !== null && preview === null && notice !== null ? 400 : 200, screen(template.name, html `<p><a href="/flows/new">New flow</a></p><h1>${template.name}</h1>${page}`, { chrome: chromeFor(repo, "flows") }));
|
|
61
|
+
}
|
|
62
|
+
// ---- path parameters and shared lookups ------------------------------------
|
|
63
|
+
/** One segment of the path the route's pattern admitted ("/flows/1/cards/2/move" → 2 is "1", 4 is "2"). */
|
|
64
|
+
const pathPart = (url, index) => url.pathname.split("/")[index] ?? "";
|
|
65
|
+
/** A flow the caller may see: active, in a visible project. */
|
|
66
|
+
const visibleFlow = (id) => {
|
|
67
|
+
const flow = store.getFlow(id);
|
|
68
|
+
return flow === null || flow.state !== "active" || !visible(flow.repo) ? null : flow;
|
|
69
|
+
};
|
|
70
|
+
/** A subagent the caller may see: not removed, in a visible project the account can access. */
|
|
71
|
+
const visibleSubagent = (who, id) => {
|
|
72
|
+
const mate = store.getSubagent(id);
|
|
73
|
+
return mate === null || mate.state === "removed" || !visible(mate.repo) || !store.accountCanAccess(who.name, mate.repo) ? null : mate;
|
|
74
|
+
};
|
|
75
|
+
const sendJson = (response, status, payload) => respond(response, status, "application/json; charset=utf-8", JSON.stringify(payload));
|
|
76
|
+
// ---- flows: reads ----------------------------------------------------------
|
|
77
|
+
// Flows → New: the gallery, and each template's page.
|
|
78
|
+
async function newFlowGallery(ctx) {
|
|
79
|
+
const { url, response, who, project } = ctx;
|
|
80
|
+
const projects = consoleProjects();
|
|
81
|
+
const canUse = who.via === "cookie" && who.role === "approver" && projects.length > 0;
|
|
82
|
+
// The chosen project, else the one being looked at: its tools are what each card says is connected.
|
|
83
|
+
const asked = url.searchParams.get("repo");
|
|
84
|
+
const repo = asked !== null && projects.includes(asked) ? asked : project !== null && projects.includes(project) ? project : null;
|
|
85
|
+
return sendScreen(response, 200, screen("New flow", html `<p><a href="/flows">Flows</a></p><h1>New flow</h1>${(galleryHtml({ repo, canUse, connections: repo === null ? null : connectionsOf(store, repo) }))}`, { chrome: chromeFor(repo ?? project, "flows") }));
|
|
86
|
+
}
|
|
87
|
+
async function galleryTemplatePage(ctx) {
|
|
88
|
+
const { url, who, response, project } = ctx;
|
|
89
|
+
const template = galleryTemplateOf(pathPart(url, 3));
|
|
90
|
+
const projects = consoleProjects();
|
|
91
|
+
if (template === null)
|
|
92
|
+
return refuse(response, who, 404, "There's no template by that name.", "/flows/new");
|
|
93
|
+
// Not in the route table: an unknown template is a 404 for anyone first.
|
|
94
|
+
if (who.via !== "cookie" || who.role !== "approver")
|
|
95
|
+
return refuse(response, who, 403, "Sign in as an approver to create flows.", "/flows/new");
|
|
96
|
+
if (projects.length === 0)
|
|
97
|
+
return refuse(response, who, 409, "Add a project first.", "/projects");
|
|
98
|
+
const asked = url.searchParams.get("repo");
|
|
99
|
+
const repo = asked !== null && projects.includes(asked) ? asked : project !== null && projects.includes(project) ? project : projects[0];
|
|
100
|
+
// Back from a Connect started on this page: how it went.
|
|
101
|
+
const problem = url.searchParams.get("problem"), said = url.searchParams.get("said");
|
|
102
|
+
return sendGalleryUse(response, who, template, projects, repo, null, template.name, problem === null ? null : problem.slice(0, 400), said === null ? null : said.slice(0, 400));
|
|
103
|
+
}
|
|
104
|
+
async function flowsPage(ctx) {
|
|
105
|
+
const { url, who, response, project } = ctx;
|
|
106
|
+
const projects = consoleProjects();
|
|
107
|
+
const flows = store.listFlows(projects);
|
|
108
|
+
return sendScreen(response, 200, screen("Flows", html `<h1>Flows</h1>${(flowsListHtml(store, flows, projects, who.via === "cookie" && who.role === "approver", url.searchParams.get("problem")))}`, { chrome: chromeFor(project, "flows"), functional: { script: FLOW_IMPORT_SCRIPT } }));
|
|
109
|
+
}
|
|
110
|
+
async function flowInsightsRead(ctx) {
|
|
111
|
+
const { url, response } = ctx;
|
|
112
|
+
const flow = visibleFlow(Number(pathPart(url, 2)));
|
|
113
|
+
if (flow === null)
|
|
114
|
+
return sendJson(response, 404, { said: "No such flow in your projects." });
|
|
115
|
+
return sendJson(response, 200, flowInsights(store, flow, clock(), Math.min(90, Math.max(1, Number(url.searchParams.get("days")) || 30))));
|
|
116
|
+
}
|
|
117
|
+
async function flowRunRead(ctx) {
|
|
118
|
+
const { url, response } = ctx;
|
|
119
|
+
const flow = visibleFlow(Number(pathPart(url, 2)));
|
|
120
|
+
if (flow === null)
|
|
121
|
+
return sendJson(response, 404, { said: "No such flow in your projects." });
|
|
122
|
+
const card = store.getFlowCard(Number(pathPart(url, 4)));
|
|
123
|
+
const run = card === null || card.flow !== flow.id ? null : store.flowStepRun(card.id, Number(pathPart(url, 5)));
|
|
124
|
+
if (run === null)
|
|
125
|
+
return sendJson(response, 404, { said: "No such run." });
|
|
126
|
+
return sendJson(response, 200, { card: run.card, entry: run.entry, script: run.script, version: run.scriptVersion, state: run.state, result: run.result, exitCode: run.exitCode, durationMs: run.durationMs, at: run.finishedAt ?? run.startedAt, log: run.log ?? "" });
|
|
127
|
+
}
|
|
128
|
+
// A flow as a file (flow-share.ts): zones, paths, trigger settings and scripts; never secrets, addresses, names or cards.
|
|
129
|
+
async function flowExportFile(ctx) {
|
|
130
|
+
const { url, who, response } = ctx;
|
|
131
|
+
const flow = visibleFlow(Number(pathPart(url, 2)));
|
|
132
|
+
if (flow === null)
|
|
133
|
+
return refuse(response, who, 404, "No such flow in your projects.", "/flows");
|
|
134
|
+
let exported;
|
|
135
|
+
try {
|
|
136
|
+
exported = exportFlow(store, flow, options.configDir ?? null);
|
|
137
|
+
}
|
|
138
|
+
catch (error) {
|
|
139
|
+
return refuse(response, who, 409, error instanceof Error ? error.message : "This flow can't be exported.", `/flows/${flow.id}`);
|
|
140
|
+
}
|
|
141
|
+
response.setHeader("Content-Disposition", `attachment; filename="${exported.fileName}"`);
|
|
142
|
+
return respond(response, 200, "application/json; charset=utf-8", exported.json);
|
|
143
|
+
}
|
|
144
|
+
async function flowPage(ctx) {
|
|
145
|
+
const { url, who, response } = ctx;
|
|
146
|
+
const flow = visibleFlow(Number(pathPart(url, 2)));
|
|
147
|
+
if (flow === null)
|
|
148
|
+
return refuse(response, who, 404, "No such flow in your projects.", "/flows");
|
|
149
|
+
const selected = Number(url.searchParams.get("card")), start = Number(url.searchParams.get("start"));
|
|
150
|
+
const view = flowView(store, flow, { name: who.name, approver: who.via === "cookie" && who.role === "approver" }, Number.isSafeInteger(selected) && selected > 0 ? selected : null, { dir: options.configDir ?? null, repos: consoleProjects(), startTrigger: Number.isSafeInteger(start) && start > 0 ? start : null,
|
|
151
|
+
sortReady: keyStatus("openrouter", providerHome).set, toolHome });
|
|
152
|
+
if (url.searchParams.get("format") === "json")
|
|
153
|
+
return respond(response, 200, "application/json; charset=utf-8", JSON.stringify(checkResponse("flowView", view)));
|
|
154
|
+
return sendScreen(response, 200, screen(flow.name, html `<p><a href="/flows">Flows</a></p><h1>${flow.name}</h1>${flowFallbackHtml(view)}`, { chrome: chromeFor(flow.repo, "flows"), workspace: { view } }));
|
|
155
|
+
}
|
|
156
|
+
// ---- starter kits: the gallery, and each kit's checklist in a project ------
|
|
157
|
+
async function kitsGallery(ctx) {
|
|
158
|
+
const { url, who, response, project } = ctx;
|
|
159
|
+
const projects = consoleProjects();
|
|
160
|
+
const canSetUp = who.via === "cookie" && who.role === "approver";
|
|
161
|
+
return sendScreen(response, 200, screen("Starter kits", html `<h1>Starter kits</h1>${(kitsGalleryHtml(store, projects, projectName, canSetUp, { problem: url.searchParams.get("problem") }))}`, { chrome: chromeFor(project, "flows") }));
|
|
162
|
+
}
|
|
163
|
+
async function kitPage(ctx) {
|
|
164
|
+
const { url, who, response } = ctx;
|
|
165
|
+
const projects = consoleProjects();
|
|
166
|
+
const canSetUp = who.via === "cookie" && who.role === "approver";
|
|
167
|
+
const kit = kitOf(pathPart(url, 2));
|
|
168
|
+
const repo = url.searchParams.get("repo") ?? (projects.length === 1 ? projects[0] : "");
|
|
169
|
+
if (kit === null || !projects.includes(repo))
|
|
170
|
+
return redirect(response, "/kits");
|
|
171
|
+
return sendScreen(response, 200, screen(kit.name, html `<h1>${kit.name}</h1>${(kitPageHtml(store, kit, repo, options.configDir ?? null, canSetUp, { said: url.searchParams.get("said"), problem: url.searchParams.get("problem") }))}`, { chrome: chromeFor(repo, "flows") }));
|
|
172
|
+
}
|
|
173
|
+
// ---- v92 / D5: the lead's subagents — the list, and one page per subagent ----
|
|
174
|
+
async function subagentsPage(ctx) {
|
|
175
|
+
const { url, who, response, project } = ctx;
|
|
176
|
+
const projects = consoleProjects();
|
|
177
|
+
return sendScreen(response, 200, screen("Subagents", html `<p><a href="/settings/lead">Lead</a></p><h1>Subagents</h1>${(subagentsListHtml(store, store.subagents(projects), projects, projectName, who.via === "cookie" && who.role === "approver", { said: url.searchParams.get("said"), problem: url.searchParams.get("problem"), adding: url.searchParams.get("add") === "1" }))}`, { chrome: chromeFor(project, "settings") }));
|
|
178
|
+
}
|
|
179
|
+
/** D5: subagents were teammates; an old link lands on the same page under Settings → Lead. */
|
|
180
|
+
async function legacySubagentLink(ctx) {
|
|
181
|
+
const { url, response } = ctx;
|
|
182
|
+
const legacy = /^\/teammates(?:\/([1-9][0-9]{0,9})(\/soul\.md)?)?$/.exec(url.pathname);
|
|
183
|
+
return redirect(response, legacy === null || legacy[1] === undefined ? "/settings/lead#subagents" : `/settings/lead/subagents/${legacy[1]}${legacy[2] ?? ""}`);
|
|
184
|
+
}
|
|
185
|
+
async function subagentPage(ctx) {
|
|
186
|
+
const { url, who, response } = ctx;
|
|
187
|
+
const mate = visibleSubagent(who, Number(pathPart(url, 4)));
|
|
188
|
+
if (mate === null)
|
|
189
|
+
return refuse(response, who, 404, "No such subagent in your projects.", "/settings/lead/subagents");
|
|
190
|
+
const approvers = store.listApprovers().map(one => one.name).filter(name => store.accountCanAccess(name, mate.repo));
|
|
191
|
+
return sendScreen(response, 200, screen(labelOf(mate), html `<p><a href="/settings/lead">Lead</a></p><h1>${labelOf(mate)}</h1>${(subagentPageHtml(store, mate, who.name, projectName, who.via === "cookie" && who.role === "approver", approvers, { said: url.searchParams.get("said"), problem: url.searchParams.get("problem"), query: url.searchParams.get("q") }))}`, { chrome: chromeFor(mate.repo, "settings") }));
|
|
192
|
+
}
|
|
193
|
+
async function subagentSoulFile(ctx) {
|
|
194
|
+
const { url, who, response } = ctx;
|
|
195
|
+
const mate = visibleSubagent(who, Number(pathPart(url, 4)));
|
|
196
|
+
if (mate === null)
|
|
197
|
+
return refuse(response, who, 404, "No such subagent in your projects.", "/settings/lead/subagents");
|
|
198
|
+
response.writeHead(200, { ...SAFETY, "Content-Type": "text/markdown; charset=utf-8", "Content-Disposition": `attachment; filename="${mate.handle}.md"` });
|
|
199
|
+
return void response.end(mate.soul);
|
|
200
|
+
}
|
|
201
|
+
async function recipeLibrary(ctx) {
|
|
202
|
+
const { who, response, project } = ctx;
|
|
203
|
+
try {
|
|
204
|
+
const revision = who.via === "cookie" ? who.session.projectRevision : 0;
|
|
205
|
+
const recent = project === null ? [] : store.handle.prepare("SELECT document,task_id,flow_id FROM workflow_preview WHERE repo=? AND (task_id IS NOT NULL OR flow_id IS NOT NULL) ORDER BY created_at DESC LIMIT 12").all(project).map(row => {
|
|
206
|
+
const d = importRecipe(String(row["document"]));
|
|
207
|
+
const task = row["task_id"] === null ? null : store.getTask(String(row["task_id"]));
|
|
208
|
+
const flow = row["flow_id"] === null ? null : store.getFlow(Number(row["flow_id"]));
|
|
209
|
+
const on = flow !== null && flow.state === "active" && store.flowTriggers(flow.id).some(one => one.kind === "schedule" && one.state === "active");
|
|
210
|
+
return { name: d.name, href: task !== null ? taskHref(task.id) : `/flows/${Number(row["flow_id"])}`, state: task?.state ?? (flow === null || flow.state !== "active" ? "Archived" : on ? "Scheduled" : "Paused") };
|
|
149
211
|
});
|
|
150
|
-
|
|
151
|
-
response.end(); });
|
|
152
|
-
return;
|
|
212
|
+
return sendScreen(response, 200, screen("Workflow recipes", recipeLibraryHtml(starterRecipes(), project === null ? [] : savedRecipes(store, who.name, project), project, revision, recent), { chrome: chromeFor(project, "recipes"), functional: { script: recipeScript() } }));
|
|
153
213
|
}
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
const
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
214
|
+
catch (error) {
|
|
215
|
+
if (error instanceof RecipeError)
|
|
216
|
+
return refuse(response, who, error.status, error.message, "/recipes");
|
|
217
|
+
throw error;
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
/** One recipe screen in the open project: the editor (start, new, edit, from a task), use, preview, export; any other address is no screen. */
|
|
221
|
+
const recipeScreen = (shown) => async (ctx) => {
|
|
222
|
+
const { url, who, response, now, project } = ctx;
|
|
223
|
+
try {
|
|
224
|
+
const revision = who.via === "cookie" ? who.session.projectRevision : 0;
|
|
225
|
+
const render = (page) => sendScreen(response, 200, screen("Workflow recipes", page, { chrome: chromeFor(project, "recipes"), functional: { script: recipeScript() } }));
|
|
226
|
+
if (project === null)
|
|
227
|
+
return refuse(response, who, 400, "Open a project to customize a recipe.", "/projects");
|
|
228
|
+
const token = url.searchParams.get("preview");
|
|
229
|
+
const preview = token === null ? null : workflowPreview(store, who.name, project, token);
|
|
230
|
+
if (token !== null && preview === null)
|
|
231
|
+
return refuse(response, who, 404, "No preview in this project.", "/recipes");
|
|
232
|
+
const sourceRevision = url.searchParams.get("revision");
|
|
233
|
+
if (sourceRevision !== null && !/^[1-9][0-9]{0,8}$/.test(sourceRevision))
|
|
234
|
+
throw new RecipeError("Choose a valid recipe version.");
|
|
235
|
+
let recipe = preview === null ? findRecipe(store, who.name, project, url.searchParams.get("recipe") ?? "", sourceRevision === null ? undefined : Number(sourceRevision))
|
|
236
|
+
: { id: preview.source, revision: 1, repo: project, document: preview.document, digest: preview.digest, author: who.name };
|
|
237
|
+
if (shown === "new") {
|
|
238
|
+
const starter = starterRecipes().find(one => one.id === "small-feature");
|
|
239
|
+
recipe = { ...starter, id: "custom", document: { ...starter.document, name: "", description: "", goal: "", outOfScope: null, acceptance: [] } };
|
|
240
|
+
}
|
|
241
|
+
if (shown === "start" && recipe?.id === "small-feature") {
|
|
242
|
+
// This starter supplies the process, but the person supplies the
|
|
243
|
+
// feature. Do not let instructional placeholder text become work.
|
|
244
|
+
recipe = { ...recipe, document: { ...recipe.document, goal: "" } };
|
|
245
|
+
}
|
|
246
|
+
if (shown === "from-task") {
|
|
247
|
+
const id = url.searchParams.get("task") ?? "";
|
|
248
|
+
const ref = store.lookupRef(id), task = store.getTask(id), scope = store.getScope(id);
|
|
249
|
+
if (ref?.repo !== project || task === null || scope === null)
|
|
250
|
+
throw new RecipeError("No scoped task in this project.", 404);
|
|
251
|
+
recipe = { id: "task-copy", revision: 1, repo: project, digest: "", author: who.name, document: {
|
|
252
|
+
format: "standing-orders-recipe", version: 1, name: task.title, description: "Scope and success checks copied from a task. Dependencies, budgets, agents, and approvals use the new work's settings.", goal: scope.goal, outOfScope: scope.outOfScope, touches: scope.touches, acceptance: scope.acceptance,
|
|
253
|
+
planning: "auto", deliverable: ref.deliverable === "report" ? "report" : "branch", schedule: null, costCeilingUsd: null,
|
|
254
|
+
} };
|
|
255
|
+
}
|
|
256
|
+
if (shown === "run") {
|
|
257
|
+
if (recipe === null || recipe.repo === null)
|
|
258
|
+
throw new RecipeError("Save a project recipe before using it here.", 404);
|
|
259
|
+
return render(recipeRunHtml(recipe, project, revision, null, undefined, url.searchParams.get("saved") === "1"));
|
|
260
|
+
}
|
|
261
|
+
if (shown === "preview" && preview !== null) {
|
|
262
|
+
if (preview.document.version === 2 || url.searchParams.get("purpose") === "recipe")
|
|
263
|
+
return render(recipeDefinitionPreviewHtml(preview, revision));
|
|
264
|
+
const agents = resolveAgentAuthority(store, project, preview.document.acceptance, now);
|
|
265
|
+
const workers = store.listRunners().filter(one => one.retiredAt === null && one.repos.includes(project) && runnerAlive(one, now));
|
|
266
|
+
const mode = store.activeMode(project, now);
|
|
267
|
+
const readiness = [
|
|
268
|
+
{ title: "Project", detail: projectName(project) },
|
|
269
|
+
{ title: agents.ok ? "Agents configured" : "Choose your agents", detail: agents.ok ? "The project has an exact agent route. The created work will show the agents it binds." : agents.problem, ...(!restricted() && !agents.ok ? { href: "/control" } : {}) },
|
|
270
|
+
{ title: workers.length ? "Worker connected" : "Worker needed", detail: workers.length ? `${workers.length} worker${workers.length === 1 ? " is" : "s are"} answering for this project.` : "Open Toolroll on the machine with this project. Work waits safely until a worker connects." },
|
|
271
|
+
{ title: "Approval", detail: mode?.signedBy === who.name ? "Your signed project policy is available. Its scope, expiry, and approval options will be checked at filing." : "Review and approve the created work before it starts.", ...(!restricted() ? { href: "/mode" } : {}) },
|
|
272
|
+
];
|
|
273
|
+
return render(workflowPreviewHtml(preview, revision, now, readiness, mode?.signedBy === who.name));
|
|
274
|
+
}
|
|
275
|
+
if (recipe === null)
|
|
276
|
+
return refuse(response, who, 404, "No recipe in this project.", "/recipes");
|
|
277
|
+
if (shown === "export") {
|
|
278
|
+
response.setHeader("Content-Disposition", 'attachment; filename="standing-orders-recipe.json"');
|
|
279
|
+
return respond(response, 200, "application/json; charset=utf-8", exportRecipe(recipe.document));
|
|
280
|
+
}
|
|
281
|
+
if (shown === "start" || shown === "new" || shown === "edit" || shown === "from-task")
|
|
282
|
+
return render(recipeEditorHtml(recipe, project, revision, null, undefined, shown === "new" || shown === "from-task" || (shown === "start" && recipe.repo !== null) || recipe.document.version === 2 || url.searchParams.get("purpose") === "recipe"));
|
|
283
|
+
return refuse(response, who, 404, "No recipe screen here.", "/recipes");
|
|
284
|
+
}
|
|
285
|
+
catch (error) {
|
|
286
|
+
if (error instanceof RecipeError)
|
|
287
|
+
return refuse(response, who, error.status, error.message, "/recipes");
|
|
288
|
+
throw error;
|
|
289
|
+
}
|
|
290
|
+
};
|
|
291
|
+
// setting a starter kit up, trying its sample card, and bringing GitHub issues in.
|
|
292
|
+
const kitAct = (step) => async (ctx) => {
|
|
293
|
+
const { url, who, response, posted } = ctx;
|
|
294
|
+
const body = readForm(posted, CONSOLE_FORMS.kit);
|
|
295
|
+
const now = clock();
|
|
296
|
+
const kit = kitOf(pathPart(url, 2));
|
|
297
|
+
const projects = consoleProjects();
|
|
298
|
+
const repo = body.get("repo") ?? "";
|
|
299
|
+
if (kit === null || !projects.includes(repo) || !store.accountCanAccess(who.name, repo))
|
|
300
|
+
return redirect(response, `/kits?problem=${encodeURIComponent("Choose one of your projects.")}`);
|
|
301
|
+
const page = (key, words) => redirect(response, `/kits/${kit.id}?repo=${encodeURIComponent(repo)}&${key}=${encodeURIComponent(words)}`);
|
|
302
|
+
if (step === "setup") {
|
|
303
|
+
const made = await setUpKit(store, kit, repo, who.name, now, options.configDir ?? null, { toolHome });
|
|
304
|
+
return page(made.ok ? "said" : "problem", made.said);
|
|
305
|
+
}
|
|
306
|
+
if (step === "github") {
|
|
307
|
+
const made = addKitGithubTrigger(store, kit, repo, who.name, now, options.configDir ?? null);
|
|
308
|
+
return page(made.ok ? "said" : "problem", made.said);
|
|
309
|
+
}
|
|
310
|
+
const tried = addKitSample(store, kit, repo, who.name, now);
|
|
311
|
+
if (!tried.ok)
|
|
312
|
+
return page("problem", tried.said);
|
|
313
|
+
// The subagent picks it up on the worker's next pass (a few seconds); its flow opens on the card.
|
|
314
|
+
try {
|
|
315
|
+
advanceFlows(store, repo, now, { evidenceRoot });
|
|
316
|
+
}
|
|
317
|
+
catch { /* the worker's next pass moves it */ }
|
|
318
|
+
return redirect(response, tried.href);
|
|
319
|
+
};
|
|
320
|
+
// ---- v92: looking after subagents, and answering their questions -----------
|
|
321
|
+
async function answerSubagentQuestionSend(ctx) {
|
|
322
|
+
const { url, who, request, response, posted } = ctx;
|
|
323
|
+
const body = readForm(posted, CONSOLE_FORMS.subagents);
|
|
324
|
+
const now = clock();
|
|
325
|
+
const wantsJson = (request.headers.accept ?? "").includes("application/json");
|
|
326
|
+
if (who.via !== "cookie")
|
|
327
|
+
return refuse(response, who, 403, "Sign in to do that.", "/settings/lead/subagents");
|
|
328
|
+
const question = store.subagentQuestion(Number(pathPart(url, 5)));
|
|
329
|
+
const card = question === null ? null : store.getFlowCard(question.card);
|
|
330
|
+
const flow = card === null ? null : store.getFlow(card.flow);
|
|
331
|
+
const done = question === null || flow === null || !visible(flow.repo) ? { ok: false, said: "No such question." }
|
|
332
|
+
: answerSubagentQuestion(store, question.id, { choice: body.get("choice"), text: body.get("text"), by: who.name, via: "web" }, now);
|
|
333
|
+
if (wantsJson)
|
|
334
|
+
return respond(response, done.ok ? 200 : 409, "application/json; charset=utf-8", JSON.stringify(done));
|
|
335
|
+
const back = question === null ? "/settings/lead/subagents" : `/settings/lead/subagents/${question.subagent}`;
|
|
336
|
+
return redirect(response, `${back}?${done.ok ? "said" : "problem"}=${encodeURIComponent(done.said)}`);
|
|
337
|
+
}
|
|
338
|
+
async function newSubagent(ctx) {
|
|
339
|
+
const { who, response, posted } = ctx;
|
|
340
|
+
const body = readForm(posted, CONSOLE_FORMS.subagents);
|
|
341
|
+
const now = clock();
|
|
342
|
+
if (who.via !== "cookie")
|
|
343
|
+
return refuse(response, who, 403, "Sign in to do that.", "/settings/lead/subagents");
|
|
344
|
+
const projects = consoleProjects();
|
|
345
|
+
const repo = body.get("repo") ?? "";
|
|
346
|
+
if (!projects.includes(repo))
|
|
347
|
+
return redirect(response, `/settings/lead/subagents?problem=${encodeURIComponent("Choose one of your projects.")}`);
|
|
348
|
+
const template = body.get("template") ?? "";
|
|
349
|
+
const made = createSubagentFrom(store, { repo, template: template === "blank" ? null : template, name: body.get("name"), soul: template === "blank" ? BLANK_SOUL.replace("name: \n", `name: ${(body.get("name") ?? "").trim() || "Sam"}\n`).replace("role: \n", "role: Assistant\n") : null, by: who.name }, now);
|
|
350
|
+
return made.ok ? redirect(response, `/settings/lead/subagents/${made.id}?said=${encodeURIComponent(made.said)}`) : redirect(response, `/settings/lead/subagents?problem=${encodeURIComponent(made.said)}`);
|
|
351
|
+
}
|
|
352
|
+
const subagentAct = (part) => async (ctx) => {
|
|
353
|
+
const { url, who, request, response, posted } = ctx;
|
|
354
|
+
const body = readForm(posted, CONSOLE_FORMS.subagents);
|
|
355
|
+
const now = clock();
|
|
356
|
+
const wantsJson = (request.headers.accept ?? "").includes("application/json");
|
|
357
|
+
if (who.via !== "cookie")
|
|
358
|
+
return refuse(response, who, 403, "Sign in to do that.", "/settings/lead/subagents");
|
|
359
|
+
const mate = visibleSubagent(who, Number(pathPart(url, 4)));
|
|
360
|
+
if (mate === null)
|
|
361
|
+
return refuse(response, who, 404, "No such subagent in your projects.", "/settings/lead/subagents");
|
|
362
|
+
const back = `/settings/lead/subagents/${mate.id}`;
|
|
363
|
+
if (part === "soul") {
|
|
364
|
+
const soul = body.get("soul") ?? "";
|
|
365
|
+
const saved = saveSoul(store, mate, soul, who.name, now);
|
|
366
|
+
if (!saved.ok) {
|
|
367
|
+
const approvers = store.listApprovers().map(one => one.name).filter(name => store.accountCanAccess(name, mate.repo));
|
|
368
|
+
return sendScreen(response, 400, screen(labelOf(mate), html `<h1>${labelOf(mate)}</h1>${subagentPageHtml(store, mate, who.name, projectName, true, approvers, { problem: saved.said, soulDraft: soul })}`, { chrome: chromeFor(mate.repo, "flows") }));
|
|
369
|
+
}
|
|
370
|
+
return redirect(response, `${back}?said=${encodeURIComponent(saved.said)}`);
|
|
371
|
+
}
|
|
372
|
+
if (part === "week") {
|
|
373
|
+
// v97: undo one of its tool calls, or send the week's report now.
|
|
374
|
+
const done = body.get("op") === "undo" ? await undoCall(store, mate, Number(body.get("id")), who.name, { toolHome }, now)
|
|
375
|
+
: sendSubagentWeeklies(store, mate.repo, now, mate.id) > 0 ? { ok: true, said: `Sent the week's report to ${mate.manager}.` } : { ok: false, said: "The report couldn't be sent." };
|
|
376
|
+
if (wantsJson)
|
|
377
|
+
return respond(response, done.ok ? 200 : 409, "application/json; charset=utf-8", JSON.stringify(done));
|
|
378
|
+
return redirect(response, `${back}?${done.ok ? "said" : "problem"}=${encodeURIComponent(done.said)}#week`);
|
|
379
|
+
}
|
|
380
|
+
if (part === "routines") {
|
|
381
|
+
// v96: its routines: add one, try one now, or remove one.
|
|
382
|
+
const op = body.get("op"), routine = Number(body.get("id"));
|
|
383
|
+
const hooks = options.configDir ?? null;
|
|
384
|
+
const changed = op === "add" ? addRoutine(store, mate, body.get("schedule") ?? "", body.get("text") ?? "", who.name, now, hooks)
|
|
385
|
+
: op === "run" ? runRoutine(store, mate, routine, who.name, now)
|
|
386
|
+
: removeRoutine(store, mate, routine, now, hooks);
|
|
387
|
+
if (changed.ok && op === "run") {
|
|
388
|
+
try {
|
|
389
|
+
advanceFlows(store, mate.repo, now, { evidenceRoot });
|
|
235
390
|
}
|
|
236
|
-
|
|
237
|
-
return render(recipeEditorHtml(recipe, project, csrf, revision, null, undefined, ["/recipes/new", "/recipes/from-task"].includes(url.pathname) || (url.pathname === "/recipes/start" && recipe.repo !== null) || recipe.document.version === 2 || url.searchParams.get("purpose") === "recipe"));
|
|
238
|
-
return refuse(response, who, 404, "No recipe screen here.", "/recipes");
|
|
239
|
-
}
|
|
240
|
-
catch (error) {
|
|
241
|
-
if (error instanceof RecipeError)
|
|
242
|
-
return refuse(response, who, error.status, error.message, "/recipes");
|
|
243
|
-
throw error;
|
|
391
|
+
catch { /* the worker's next pass moves it */ }
|
|
244
392
|
}
|
|
393
|
+
return redirect(response, `${back}?${changed.ok ? "said" : "problem"}=${encodeURIComponent(changed.said)}#desk`);
|
|
394
|
+
}
|
|
395
|
+
if (part === "memory") {
|
|
396
|
+
// v95: edit or forget one thing it remembers.
|
|
397
|
+
const memory = Number(body.get("id"));
|
|
398
|
+
const changed = body.get("op") === "forget" ? forgetMemory(store, mate, memory, who.name, now) : editMemory(store, mate, memory, body.get("text") ?? "", who.name, now);
|
|
399
|
+
return redirect(response, `${back}?${changed.ok ? "said" : "problem"}=${encodeURIComponent(changed.said)}#memory`);
|
|
400
|
+
}
|
|
401
|
+
if (part === "tools") {
|
|
402
|
+
// v94: which project tools it may use, and its rule for each action.
|
|
403
|
+
const tool = body.get("tool") ?? "", op = body.get("op");
|
|
404
|
+
const grant = store.subagentGrant(mate.id, tool);
|
|
405
|
+
const changed = op === "grant" ? await grantTool(store, mate, tool, who.name, now, { toolHome })
|
|
406
|
+
: op === "revoke" ? revokeTool(store, mate, tool, who.name, now)
|
|
407
|
+
: grant === null ? { ok: false, said: `${nameOf(mate)} doesn't use ${tool}.` } : setToolRules(store, mate, tool, rulesFromForm(grant, key => body.sent.get(key)), who.name, now);
|
|
408
|
+
return redirect(response, `${back}?${changed.ok ? "said" : "problem"}=${encodeURIComponent(changed.said)}#tools`);
|
|
409
|
+
}
|
|
410
|
+
const done = part === "state" ? setSubagentState(store, mate, body.get("state") === "removed" ? "removed" : body.get("state") === "paused" ? "paused" : "active", who.name, now)
|
|
411
|
+
: part === "note" ? tellSubagent(store, mate, body.get("note") ?? "", who.name, now)
|
|
412
|
+
: part === "settings" ? subagentSettings(store, mate, { model: body.get("model") ?? "default", dailyTurns: Number(body.get("dailyTurns")), manager: body.get("manager") ?? mate.manager }, who.name, now)
|
|
413
|
+
: sendSubagentSummaries(store, mate.repo, now, mate.id) > 0 ? { ok: true, said: `Sent today's summary to ${mate.manager}.` } : { ok: false, said: "The summary couldn't be sent." };
|
|
414
|
+
if (part === "state" && body.get("state") === "removed" && done.ok)
|
|
415
|
+
return redirect(response, `/settings/lead/subagents?said=${encodeURIComponent(done.said)}`);
|
|
416
|
+
return redirect(response, `${back}?${done.ok ? "said" : "problem"}=${encodeURIComponent(done.said)}${part === "note" ? "#memory" : ""}`);
|
|
417
|
+
};
|
|
418
|
+
// ---- flows: making one -----------------------------------------------------
|
|
419
|
+
// Flows → New → a template: preview these answers, or create exactly what was previewed.
|
|
420
|
+
async function createFromGallery(ctx) {
|
|
421
|
+
const { url, who, response, posted } = ctx;
|
|
422
|
+
const body = readForm(posted, CONSOLE_FORMS.galleryUse);
|
|
423
|
+
const template = galleryTemplateOf(pathPart(url, 3));
|
|
424
|
+
const projects = consoleProjects();
|
|
425
|
+
if (template === null)
|
|
426
|
+
return refuse(response, who, 404, "There's no template by that name.", "/flows/new");
|
|
427
|
+
// Not in the route table: an unknown template is a 404 for anyone first, and a demo creates nothing.
|
|
428
|
+
if (who.via !== "cookie" || who.role !== "approver" || store.isDemo())
|
|
429
|
+
return refuse(response, who, 403, "Sign in as an approver to create flows.", "/flows/new");
|
|
430
|
+
const repo = body.get("repo") ?? "";
|
|
431
|
+
if (!projects.includes(repo))
|
|
432
|
+
return refuse(response, who, 404, "Choose one of your projects.", "/flows/new");
|
|
433
|
+
const answers = { ...Object.fromEntries(template.asks.map(ask => [ask.key, body.get(ask.key) ?? ""])), ...(body.get("send-result") === "yes" ? { "send-result": "yes" } : {}) };
|
|
434
|
+
const name = (body.get("name") ?? "").replace(/[\u0000-\u001f\u007f]/g, "").trim().slice(0, 80) || template.name;
|
|
435
|
+
if (body.get("intent") !== "create")
|
|
436
|
+
return sendGalleryUse(response, who, template, projects, repo, answers, name, null);
|
|
437
|
+
// What's made is what was previewed: changed answers are previewed again first.
|
|
438
|
+
let digest = null;
|
|
439
|
+
try {
|
|
440
|
+
digest = previewGallery(store, template, repo, answers, who.name, clock()).digest;
|
|
245
441
|
}
|
|
246
|
-
|
|
442
|
+
catch {
|
|
443
|
+
digest = null;
|
|
444
|
+
}
|
|
445
|
+
if (digest === null || digest !== body.get("previewed"))
|
|
446
|
+
return sendGalleryUse(response, who, template, projects, repo, answers, name, digest === null ? null : "Your answers changed. Check the preview, then create the flow.");
|
|
447
|
+
const used = useGalleryTemplate(store, template, repo, answers, { name, by: who.name, now: clock(), dir: options.configDir ?? null });
|
|
448
|
+
if (!used.ok)
|
|
449
|
+
return sendGalleryUse(response, who, template, projects, repo, answers, name, used.said);
|
|
450
|
+
return redirect(response, `/flows/${used.flow}`);
|
|
247
451
|
}
|
|
248
|
-
async function
|
|
249
|
-
const {
|
|
250
|
-
|
|
251
|
-
const
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
452
|
+
async function createFlow(ctx) {
|
|
453
|
+
const { who, response, posted } = ctx;
|
|
454
|
+
const body = readForm(posted, CONSOLE_FORMS.flows);
|
|
455
|
+
const now = clock();
|
|
456
|
+
const projects = consoleProjects();
|
|
457
|
+
const repo = body.get("repo") ?? "";
|
|
458
|
+
if (!projects.includes(repo))
|
|
459
|
+
return redirect(response, `/flows?problem=${encodeURIComponent("Choose one of your projects.")}`);
|
|
460
|
+
const template = FLOW_TEMPLATES.find(one => one.id === body.get("template")) ?? FLOW_TEMPLATES[0];
|
|
461
|
+
const name = (body.get("name") ?? "").replace(/[\u0000-\u001f\u007f]/g, "").trim().slice(0, 80) || template.label;
|
|
462
|
+
const made = store.createFlow({ repo, name, definitionJson: JSON.stringify(template.definition), by: who.name }, now);
|
|
463
|
+
// A template that starts from something (Issues to PRs: labelled issues) comes with its trigger.
|
|
464
|
+
const trigger = template.trigger === undefined ? null : addFlowTriggerTo(store, store.getFlow(made), template.trigger, who.name, now, options.configDir ?? null);
|
|
465
|
+
if (trigger !== null && !trigger.ok)
|
|
466
|
+
return redirect(response, `/flows?problem=${encodeURIComponent(`${name} was made, but its trigger wasn't: ${trigger.message} Add one on its Triggers panel.`)}`);
|
|
467
|
+
return redirect(response, `/flows/${made}`);
|
|
468
|
+
}
|
|
469
|
+
// A first look (v88): the Email replies template with one sample question, which Claude drafts a reply to straight away.
|
|
470
|
+
async function createExampleFlow(ctx) {
|
|
471
|
+
const { who, response, posted } = ctx;
|
|
472
|
+
const body = readForm(posted, CONSOLE_FORMS.flows);
|
|
473
|
+
const now = clock();
|
|
474
|
+
const projects = consoleProjects();
|
|
475
|
+
const repo = body.get("repo") ?? "";
|
|
476
|
+
if (!projects.includes(repo))
|
|
477
|
+
return redirect(response, `/flows?problem=${encodeURIComponent("Choose one of your projects.")}`);
|
|
478
|
+
const template = FLOW_TEMPLATES.find(one => one.id === "email-replies");
|
|
479
|
+
const id = store.createFlow({ repo, name: "Customer replies (example)", definitionJson: JSON.stringify(template.definition), by: who.name }, now);
|
|
480
|
+
addCardToFlow(store, store.getFlow(id), { title: "Do you ship to Canada?", description: "Hi! I'm thinking of ordering but I live in Toronto. Do you ship there, and how long does it take? — sam@example.com", stage: null }, who.name, now);
|
|
481
|
+
return redirect(response, `/flows/${id}`);
|
|
482
|
+
}
|
|
483
|
+
// A flow file, chosen or fetched from a gist: previewed in plain words, then made with its triggers off and its scripts held.
|
|
484
|
+
async function importFlowFile(ctx) {
|
|
485
|
+
const { who, response, posted } = ctx;
|
|
486
|
+
const body = readForm(posted, CONSOLE_FORMS.flows);
|
|
487
|
+
const now = clock();
|
|
488
|
+
const projects = consoleProjects();
|
|
489
|
+
const repo = body.get("repo") ?? "";
|
|
490
|
+
const back = (problem) => redirect(response, `/flows?problem=${encodeURIComponent(problem)}`);
|
|
491
|
+
if (!projects.includes(repo))
|
|
492
|
+
return back("Choose one of your projects.");
|
|
493
|
+
let document = body.get("document") ?? "";
|
|
494
|
+
const address = (body.get("url") ?? "").trim();
|
|
495
|
+
let file;
|
|
496
|
+
try {
|
|
497
|
+
if (document.trim() === "" && address !== "")
|
|
498
|
+
document = await fetchFlowFile(address, options.flowFetch);
|
|
499
|
+
if (document.trim() === "")
|
|
500
|
+
return back("Choose a flow file or give its address.");
|
|
501
|
+
file = parseFlowFile(document);
|
|
502
|
+
}
|
|
503
|
+
catch (error) {
|
|
504
|
+
if (error instanceof FlowFileError)
|
|
505
|
+
return back(error.message);
|
|
506
|
+
throw error;
|
|
507
|
+
}
|
|
508
|
+
const given = Object.fromEntries(file.parameters.flatMap(one => body.has(`param.${one.id}`) ? [[one.id, body.get(`param.${one.id}`) ?? ""]] : []));
|
|
509
|
+
// What was previewed: the file and every value it resolved to (a default the first preview filled in included).
|
|
510
|
+
const previewedOf = (plan) => createHash("sha256").update(`${document}\0${JSON.stringify(plan?.values ?? given)}`).digest("hex").slice(0, 32);
|
|
511
|
+
const csrf = who.via === "cookie" ? who.session.csrf : "";
|
|
512
|
+
const page = (plan, problem, status) => sendScreen(response, status, screen(`Import ${file.name}`, html `<p><a href="/flows">Flows</a></p><h1>Import ${file.name}</h1>${(flowImportHtml({ plan, file, document, repo, csrf, values: given, previewed: previewedOf(plan), problem }))}`, { chrome: chromeFor(repo, "flows") }));
|
|
513
|
+
let plan;
|
|
514
|
+
try {
|
|
515
|
+
plan = planFlowImport(store, repo, file, given, who.name);
|
|
516
|
+
}
|
|
517
|
+
catch (error) {
|
|
518
|
+
if (error instanceof FlowFileError)
|
|
519
|
+
return page(null, error.message, 400);
|
|
520
|
+
throw error;
|
|
521
|
+
}
|
|
522
|
+
if (body.get("confirm") === "yes") {
|
|
523
|
+
if (body.get("previewed") !== previewedOf(plan))
|
|
524
|
+
return page(plan, "What you filled in changed. Check the preview again, then import.", 409);
|
|
275
525
|
try {
|
|
276
|
-
|
|
277
|
-
}
|
|
278
|
-
catch { /* the worker's next pass moves it */ }
|
|
279
|
-
return redirect(response, tried.href);
|
|
280
|
-
}
|
|
281
|
-
// v92: looking after teammates, and answering their questions.
|
|
282
|
-
const teammatePost = url.pathname === "/teammates/new" ? ["", "0", "new"] : /^\/teammates\/([1-9][0-9]{0,9})\/(soul|state|note|settings|summary|tools|memory|routines|week)$/.exec(url.pathname);
|
|
283
|
-
const questionPost = /^\/teammates\/questions\/([1-9][0-9]{0,9})\/answer$/.exec(url.pathname);
|
|
284
|
-
if (teammatePost !== null || questionPost !== null) {
|
|
285
|
-
const body = readForm(posted, CONSOLE_FORMS.teammates);
|
|
286
|
-
const now = clock();
|
|
287
|
-
const wantsJson = (request.headers.accept ?? "").includes("application/json");
|
|
288
|
-
if (who.via !== "cookie")
|
|
289
|
-
return refuse(response, who, 403, "Sign in to do that.", "/teammates");
|
|
290
|
-
if (questionPost !== null) {
|
|
291
|
-
const question = store.teammateQuestion(Number(questionPost[1]));
|
|
292
|
-
const card = question === null ? null : store.getFlowCard(question.card);
|
|
293
|
-
const flow = card === null ? null : store.getFlow(card.flow);
|
|
294
|
-
const done = question === null || flow === null || !visible(flow.repo) ? { ok: false, said: "No such question." }
|
|
295
|
-
: answerTeammateQuestion(store, question.id, { choice: body.get("choice"), text: body.get("text"), by: who.name, via: "web" }, now);
|
|
296
|
-
if (wantsJson)
|
|
297
|
-
return respond(response, done.ok ? 200 : 409, "application/json; charset=utf-8", JSON.stringify(done));
|
|
298
|
-
const back = question === null ? "/teammates" : `/teammates/${question.teammate}`;
|
|
299
|
-
return redirect(response, `${back}?${done.ok ? "said" : "problem"}=${encodeURIComponent(done.said)}`);
|
|
526
|
+
return redirect(response, `/flows/${importFlow(store, plan, who.name, now, options.configDir ?? null).id}`);
|
|
300
527
|
}
|
|
301
|
-
|
|
302
|
-
return
|
|
303
|
-
if (teammatePost[2] === "new") {
|
|
304
|
-
const projects = consoleProjects();
|
|
305
|
-
const repo = body.get("repo") ?? "";
|
|
306
|
-
if (!projects.includes(repo))
|
|
307
|
-
return redirect(response, `/teammates?problem=${encodeURIComponent("Choose one of your projects.")}`);
|
|
308
|
-
const template = body.get("template") ?? "";
|
|
309
|
-
const made = createTeammateFrom(store, { repo, template: template === "blank" ? null : template, name: body.get("name"), soul: template === "blank" ? BLANK_SOUL.replace("name: \n", `name: ${(body.get("name") ?? "").trim() || "Sam"}\n`).replace("role: \n", "role: Assistant\n") : null, by: who.name }, now);
|
|
310
|
-
return made.ok ? redirect(response, `/teammates/${made.id}?said=${encodeURIComponent(made.said)}`) : redirect(response, `/teammates?problem=${encodeURIComponent(made.said)}`);
|
|
311
|
-
}
|
|
312
|
-
const mate = store.getTeammate(Number(teammatePost[1]));
|
|
313
|
-
if (mate === null || mate.state === "removed" || !visible(mate.repo) || !store.accountCanAccess(who.name, mate.repo))
|
|
314
|
-
return refuse(response, who, 404, "No such teammate in your projects.", "/teammates");
|
|
315
|
-
const back = `/teammates/${mate.id}`;
|
|
316
|
-
const action = teammatePost[2];
|
|
317
|
-
if (action === "soul") {
|
|
318
|
-
const soul = body.get("soul") ?? "";
|
|
319
|
-
const saved = saveSoul(store, mate, soul, who.name, now);
|
|
320
|
-
if (!saved.ok) {
|
|
321
|
-
const approvers = store.listApprovers().map(one => one.name).filter(name => store.accountCanAccess(name, mate.repo));
|
|
322
|
-
return sendScreen(response, 400, screen(labelOf(mate), `<h1>${escape(labelOf(mate))}</h1>${teammatePageHtml(store, mate, who.name, projectName, who.session.csrf, true, approvers, { problem: saved.said, soulDraft: soul })}`, { chrome: chromeFor(mate.repo, "flows") }));
|
|
323
|
-
}
|
|
324
|
-
return redirect(response, `${back}?said=${encodeURIComponent(saved.said)}`);
|
|
528
|
+
catch (error) {
|
|
529
|
+
return page(plan, error instanceof Error ? error.message : "That flow couldn't be imported.", 400);
|
|
325
530
|
}
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
531
|
+
}
|
|
532
|
+
return page(plan, null, 200);
|
|
533
|
+
}
|
|
534
|
+
// ---- flows: changing one (answers the editor's JSON) -----------------------
|
|
535
|
+
/** What every change to one flow starts from: its form, the flow (else the editor hears 404), and how a change settles. Null once answered. */
|
|
536
|
+
function openFlowChange(ctx) {
|
|
537
|
+
const { url, who, response, posted } = ctx;
|
|
538
|
+
const body = readForm(posted, CONSOLE_FORMS.flows);
|
|
539
|
+
const now = clock();
|
|
540
|
+
const answer = (status, payload) => sendJson(response, status, payload);
|
|
541
|
+
const projects = consoleProjects();
|
|
542
|
+
const flow = visibleFlow(Number(pathPart(url, 2)));
|
|
543
|
+
if (flow === null) {
|
|
544
|
+
answer(404, { ok: false, said: "No such flow in your projects." });
|
|
545
|
+
return null;
|
|
546
|
+
}
|
|
547
|
+
const definition = flowDefinitionOf(flow);
|
|
548
|
+
const dir = options.configDir ?? null;
|
|
549
|
+
const viewNow = () => flowView(store, store.getFlow(flow.id) ?? flow, { name: who.name, approver: true }, null, { dir, repos: projects, sortReady: keyStatus("openrouter", providerHome).set, toolHome });
|
|
550
|
+
const settle = (said, extra = {}) => {
|
|
551
|
+
// Move what can move right away (a message, a decision's next zone); workers file and run the tasks.
|
|
552
|
+
try {
|
|
553
|
+
advanceFlows(store, flow.repo, now, { evidenceRoot });
|
|
333
554
|
}
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
555
|
+
catch { /* the next worker pass retries */ }
|
|
556
|
+
return answer(200, { ok: true, said, view: viewNow(), ...extra });
|
|
557
|
+
};
|
|
558
|
+
return { body, now, answer, projects, flow, definition, dir, viewNow, settle };
|
|
559
|
+
}
|
|
560
|
+
const flowAct = (action) => async (ctx) => {
|
|
561
|
+
const { who, response } = ctx;
|
|
562
|
+
const change = openFlowChange(ctx);
|
|
563
|
+
if (change === null)
|
|
564
|
+
return;
|
|
565
|
+
const { body, now, answer, flow, definition, dir, settle } = change;
|
|
566
|
+
if (action === "triggers") {
|
|
567
|
+
let raw;
|
|
568
|
+
try {
|
|
569
|
+
raw = JSON.parse(body.get("trigger") ?? "null");
|
|
348
570
|
}
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
const memory = Number(body.get("id"));
|
|
352
|
-
const changed = body.get("op") === "forget" ? forgetMemory(store, mate, memory, who.name, now) : editMemory(store, mate, memory, body.get("text") ?? "", who.name, now);
|
|
353
|
-
return redirect(response, `${back}?${changed.ok ? "said" : "problem"}=${encodeURIComponent(changed.said)}#memory`);
|
|
571
|
+
catch {
|
|
572
|
+
return answer(400, { ok: false, said: "That trigger couldn't be read." });
|
|
354
573
|
}
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
574
|
+
const made = addFlowTriggerTo(store, flow, raw, who.name, now, dir);
|
|
575
|
+
return made.ok ? settle(made.said, made.reveal === null ? {} : { reveal: made.reveal }) : answer(400, { ok: false, said: made.message });
|
|
576
|
+
}
|
|
577
|
+
if (action === "scripts") {
|
|
578
|
+
// The project's script library, reached from any of its flows.
|
|
579
|
+
if (body.get("approve") === "yes")
|
|
580
|
+
return store.approveFlowScript(flow.repo, body.get("name") ?? "") ? settle("Approved. Zones that run it go on.") : answer(404, { ok: false, said: "That script isn't waiting for approval." });
|
|
581
|
+
if (body.get("remove") === "yes")
|
|
582
|
+
return store.removeFlowScript(flow.repo, body.get("name") ?? "") ? settle("Script removed. Zones that ran it wait until it's back.") : answer(404, { ok: false, said: "There's no script by that name." });
|
|
583
|
+
const saved = saveScript(store, flow.repo, { name: body.get("name"), about: body.get("about"), body: body.get("body"), timeoutMinutes: body.get("timeoutMinutes"), language: body.get("language"), file: body.get("file") }, who.name, now);
|
|
584
|
+
return saved.ok ? settle(saved.said) : answer(400, { ok: false, said: saved.message });
|
|
585
|
+
}
|
|
586
|
+
if (action === "linear-key" || action === "hooks-address") {
|
|
587
|
+
// Installation settings, set from the flow they are needed on.
|
|
588
|
+
if (dir === null)
|
|
589
|
+
return answer(409, { ok: false, said: "These settings are kept on the console's computer." });
|
|
590
|
+
if (action === "hooks-address") {
|
|
591
|
+
const saved = saveHooksBase(dir, body.get("address") ?? "");
|
|
592
|
+
return saved.ok ? settle(saved.base === null ? "Public address cleared." : "Public address saved.") : answer(400, { ok: false, said: saved.message });
|
|
593
|
+
}
|
|
594
|
+
if (body.get("remove") === "yes") {
|
|
595
|
+
removeLinearKey(dir);
|
|
596
|
+
return settle("Linear key removed.");
|
|
597
|
+
}
|
|
598
|
+
if (!authenticateApprover(who, body.get("password") ?? "").ok)
|
|
599
|
+
return answer(403, { ok: false, said: "Enter your Toolroll password to save a key." });
|
|
600
|
+
const saved = saveLinearKey(dir, body.get("key") ?? "");
|
|
601
|
+
return saved.ok ? settle("Linear key saved on this computer.") : answer(400, { ok: false, said: saved.message });
|
|
602
|
+
}
|
|
603
|
+
if (action === "archive") {
|
|
604
|
+
store.archiveFlow(flow.id, who.name, now);
|
|
605
|
+
return redirect(response, "/flows");
|
|
606
|
+
}
|
|
607
|
+
if (action === "save") {
|
|
608
|
+
let saved, drawn;
|
|
609
|
+
try {
|
|
610
|
+
drawn = JSON.parse(body.get("definition") ?? "null");
|
|
611
|
+
saved = validateFlowDefinition(drawn);
|
|
612
|
+
// A {{stage.…}} this save adds must be one its zone hands on; ones the saved flow had stay.
|
|
613
|
+
const references = stageReferenceProblems(saved, flowDefinitionOf(flow));
|
|
614
|
+
if (references.length > 0)
|
|
615
|
+
throw new FlowContractError(references.map(one => one.line));
|
|
363
616
|
}
|
|
364
|
-
|
|
365
|
-
:
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
if (
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
const
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
617
|
+
catch (error) {
|
|
618
|
+
return answer(400, { ok: false, said: error instanceof SyntaxError ? "That flow couldn't be read." : error instanceof FlowContractError ? withZoneNames(error.lines, drawn) : error instanceof Error ? error.message : "That flow isn't valid." });
|
|
619
|
+
}
|
|
620
|
+
const name = (body.get("name") ?? flow.name).replace(/[\u0000-\u001f\u007f]/g, "").trim().slice(0, 80) || flow.name;
|
|
621
|
+
// The owner (v86) is whom "the owner decides" zones ask: someone who can approve on this project.
|
|
622
|
+
const owner = (body.get("owner") ?? "").trim();
|
|
623
|
+
if (owner !== "" && owner !== flow.owner && !(store.listApprovers().some(one => one.name === owner) && store.accountCanAccess(owner, flow.repo)))
|
|
624
|
+
return answer(400, { ok: false, said: `${owner} can't approve on this project, so they can't own this flow.` });
|
|
625
|
+
// A build zone in another project files work there as the flow's owner: only in one they may file in.
|
|
626
|
+
const elsewhere = crossProjectProblem(store, saved, { repo: flow.repo, owner: owner || flow.owner }, who.name);
|
|
627
|
+
if (elsewhere !== null)
|
|
628
|
+
return answer(400, { ok: false, said: elsewhere });
|
|
629
|
+
if (!store.saveFlow(flow.id, { name, definitionJson: JSON.stringify(saved), sawRevision: Number(body.get("revision")), by: who.name }, now))
|
|
630
|
+
return answer(409, { ok: false, said: "Someone else changed this flow. Reload to see their changes, then make yours again." });
|
|
631
|
+
if (owner !== "" && owner !== flow.owner)
|
|
632
|
+
store.setFlowOwner(flow.id, owner, now);
|
|
633
|
+
return settle("Saved.");
|
|
634
|
+
}
|
|
635
|
+
if (action === "secrets") {
|
|
636
|
+
// A web request's secret: written here, used only in its headers, never shown back.
|
|
637
|
+
if (dir === null)
|
|
638
|
+
return answer(409, { ok: false, said: "Secrets are kept on the console's computer." });
|
|
639
|
+
const saved = setFlowSecret(dir, flow.repo, (body.get("name") ?? "").trim(), body.get("value") ?? "");
|
|
640
|
+
return saved.ok ? settle(saved.said) : answer(400, { ok: false, said: saved.message });
|
|
641
|
+
}
|
|
642
|
+
// Adding a card.
|
|
643
|
+
if (definition === null)
|
|
644
|
+
return answer(409, { ok: false, said: "This flow's drawing can't be read. Save it again from the editor." });
|
|
645
|
+
const added = addCardToFlow(store, flow, { title: body.get("title"), description: body.get("description"), stage: body.get("stage") }, who.name, now);
|
|
646
|
+
return added.ok ? settle("Card added.") : answer(400, { ok: false, said: added.message });
|
|
647
|
+
};
|
|
648
|
+
const cardAct = (verb) => async (ctx) => {
|
|
649
|
+
const { url, who } = ctx;
|
|
650
|
+
const change = openFlowChange(ctx);
|
|
651
|
+
if (change === null)
|
|
652
|
+
return;
|
|
653
|
+
const { body, now, answer, projects, flow, definition, settle } = change;
|
|
654
|
+
if (definition === null)
|
|
655
|
+
return answer(409, { ok: false, said: "This flow's drawing can't be read. Save it again from the editor." });
|
|
656
|
+
const target = store.getFlowCard(Number(pathPart(url, 4)));
|
|
657
|
+
if (target === null || target.flow !== flow.id)
|
|
658
|
+
return answer(404, { ok: false, said: "That card isn't in this flow." });
|
|
659
|
+
if (verb === "move") {
|
|
660
|
+
if (!definition.stages.some(one => one.id === body.get("stage")))
|
|
661
|
+
return answer(400, { ok: false, said: "Choose a zone in this flow." });
|
|
662
|
+
const moved = moveCardInFlow(store, target, body.get("stage") ?? "", who.name, now);
|
|
663
|
+
return !moved.ok ? answer(409, { ok: false, said: moved.message }) : moved.said === "Already there." ? answer(200, { ok: true, said: moved.said }) : settle(moved.said);
|
|
664
|
+
}
|
|
665
|
+
if (verb === "decide") {
|
|
666
|
+
const decision = body.get("decision") === "approve" ? "approve" : body.get("decision") === "send-back" ? "send-back" : null;
|
|
667
|
+
if (decision === null)
|
|
668
|
+
return answer(400, { ok: false, said: "Approve it or send it back." });
|
|
669
|
+
const note = (body.get("note") ?? "").trim() || null;
|
|
670
|
+
if (note !== null && note.length > LIMITS.note)
|
|
671
|
+
return answer(400, { ok: false, said: `Keep the note to ${LIMITS.note.toLocaleString("en-US")} characters; this is ${note.length.toLocaleString("en-US")}.` });
|
|
672
|
+
const decided = decideFlowCard(store, { card: target.id, decision, note, actor: who.name, repos: projects, evidenceRoot, draft: body.get("draft"), where: "the console" }, now);
|
|
673
|
+
return decided.ok ? settle(decided.said) : answer(409, { ok: false, said: decided.message });
|
|
674
|
+
}
|
|
675
|
+
if (verb === "choose") {
|
|
676
|
+
// "Person chooses": one of the zone's options (by its place and words), or a reply that becomes the note.
|
|
677
|
+
const choice = /^[0-9]$/.test(body.get("choice") ?? "") ? Number(body.get("choice")) : null;
|
|
678
|
+
const chosen = chooseFlowCard(store, { card: target.id, ...(/^[1-9][0-9]{0,9}$/.test(body.get("entry") ?? "") ? { entry: Number(body.get("entry")) } : {}), choice,
|
|
679
|
+
...(choice !== null && body.get("label") !== null ? { label: body.get("label") } : {}), note: choice === null ? body.get("note") : null, actor: who.name, where: "Toolroll", repos: projects, evidenceRoot }, now);
|
|
680
|
+
return chosen.ok ? settle(chosen.said) : answer(409, { ok: false, said: chosen.message });
|
|
681
|
+
}
|
|
682
|
+
if (verb === "comment") {
|
|
683
|
+
const commented = commentOnFlowCard(store, target, who.name, body.get("body"), now);
|
|
684
|
+
return commented.ok ? settle(commented.said) : answer(400, { ok: false, said: commented.message });
|
|
685
|
+
}
|
|
686
|
+
if (verb === "assign") {
|
|
687
|
+
const assigned = assignFlowCard(store, target, body.get("owner") || null, who.name, now);
|
|
688
|
+
return assigned.ok ? settle(assigned.said) : answer(400, { ok: false, said: assigned.message });
|
|
689
|
+
}
|
|
690
|
+
if (verb === "watch")
|
|
691
|
+
return settle(watchFlowCard(store, target, who.name, body.get("watching") !== "no", now).ok ? body.get("watching") === "no" ? "You won't hear about this card unless someone mentions you." : "You'll hear about this card." : "Done.");
|
|
692
|
+
const cancelled = cancelFlowCard(store, target, who.name, now);
|
|
693
|
+
return cancelled.ok ? settle(cancelled.said) : answer(409, { ok: false, said: cancelled.message });
|
|
694
|
+
};
|
|
695
|
+
const triggerAct = (verb) => async (ctx) => {
|
|
696
|
+
const { url, who } = ctx;
|
|
697
|
+
const change = openFlowChange(ctx);
|
|
698
|
+
if (change === null)
|
|
699
|
+
return;
|
|
700
|
+
const { body, now, answer, flow, dir, viewNow, settle } = change;
|
|
701
|
+
const trigger = store.getFlowTrigger(Number(pathPart(url, 4)));
|
|
702
|
+
if (trigger === null || trigger.flow !== flow.id || trigger.state === "removed")
|
|
703
|
+
return answer(404, { ok: false, said: "That trigger isn't on this flow." });
|
|
704
|
+
if (verb === "pause" || verb === "resume") {
|
|
705
|
+
setFlowTriggerOn(store, trigger, verb === "resume", now);
|
|
706
|
+
return settle(verb === "pause" ? "Trigger paused." : "Trigger on again.");
|
|
707
|
+
}
|
|
708
|
+
if (verb === "remove") {
|
|
709
|
+
removeFlowTrigger(store, trigger, now, dir);
|
|
710
|
+
return settle("Trigger removed.");
|
|
711
|
+
}
|
|
712
|
+
if (verb === "share") {
|
|
713
|
+
const shared = shareFlowButton(store, trigger, now, dir);
|
|
714
|
+
return shared.ok ? settle(shared.said, { reveal: shared.reveal }) : answer(409, { ok: false, said: shared.message });
|
|
715
|
+
}
|
|
716
|
+
if (verb === "unshare") {
|
|
717
|
+
stopSharingFlowButton(store, trigger, now);
|
|
718
|
+
return settle("The form link no longer works.");
|
|
719
|
+
}
|
|
720
|
+
if (verb === "press") {
|
|
721
|
+
let answers;
|
|
391
722
|
try {
|
|
392
|
-
|
|
723
|
+
answers = JSON.parse(body.get("answers") ?? "[]");
|
|
393
724
|
}
|
|
394
725
|
catch {
|
|
395
|
-
|
|
726
|
+
answers = [];
|
|
396
727
|
}
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
if (url.pathname === "/flows/new" || url.pathname === "/flows/example" || url.pathname === "/flows/import" || flowPost !== null || triggerPost !== null) {
|
|
407
|
-
const body = readForm(posted, CONSOLE_FORMS.flows);
|
|
408
|
-
const now = clock();
|
|
409
|
-
const answer = (status, payload) => respond(response, status, "application/json; charset=utf-8", JSON.stringify(payload));
|
|
410
|
-
const projects = consoleProjects();
|
|
411
|
-
if (who.via !== "cookie" || who.role !== "approver")
|
|
412
|
-
return url.pathname === "/flows/new" || url.pathname === "/flows/example" || url.pathname === "/flows/import" ? refuse(response, who, 403, "Sign in as an approver to create flows.", "/flows") : answer(403, { ok: false, said: "Sign in as an approver to change flows." });
|
|
413
|
-
if (url.pathname === "/flows/example") {
|
|
414
|
-
// A first look (v88): the Email replies template with one sample question, which Claude drafts a reply to straight away.
|
|
415
|
-
const repo = body.get("repo") ?? "";
|
|
416
|
-
if (!projects.includes(repo))
|
|
417
|
-
return redirect(response, `/flows?problem=${encodeURIComponent("Choose one of your projects.")}`);
|
|
418
|
-
const template = FLOW_TEMPLATES.find(one => one.id === "email-replies");
|
|
419
|
-
const id = store.createFlow({ repo, name: "Customer replies (example)", definitionJson: JSON.stringify(template.definition), by: who.name }, now);
|
|
420
|
-
addCardToFlow(store, store.getFlow(id), { title: "Do you ship to Canada?", description: "Hi! I'm thinking of ordering but I live in Toronto. Do you ship there, and how long does it take? — sam@example.com", stage: null }, who.name, now);
|
|
421
|
-
return redirect(response, `/flows/${id}`);
|
|
728
|
+
const pressed = pressFlowButton(store, trigger, Array.isArray(answers) ? answers : [], who.name, now);
|
|
729
|
+
return pressed.ok ? settle(pressed.said) : answer(400, { ok: false, said: pressed.message });
|
|
730
|
+
}
|
|
731
|
+
if (verb === "check") {
|
|
732
|
+
const checked = await checkFlowTriggerNow(store, trigger, now, { gh: options.flowTriggerIo?.gh ?? execRun, fetch: options.flowTriggerIo?.fetch ?? fetch, dir, ...(options.flowTriggerIo?.mail === undefined ? {} : { mail: options.flowTriggerIo.mail }),
|
|
733
|
+
// A schedule's script, run now (v90): in a clean folder beside the database, inside the agents' fence.
|
|
734
|
+
shell: options.flowTriggerIo?.shell ?? execRun, scratch: options.flowTriggerIo?.scratch ?? join(dir ?? tmpdir(), "flow-scratch") });
|
|
735
|
+
try {
|
|
736
|
+
advanceFlows(store, flow.repo, now, { evidenceRoot });
|
|
422
737
|
}
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
738
|
+
catch { /* the next worker pass retries */ }
|
|
739
|
+
return answer(checked.ok ? 200 : 409, { ok: checked.ok, said: checked.said, view: viewNow() });
|
|
740
|
+
}
|
|
741
|
+
if (dir === null)
|
|
742
|
+
return answer(409, { ok: false, said: "Webhook secrets are kept on the console's computer." });
|
|
743
|
+
if (verb === "renew") {
|
|
744
|
+
const renewed = renewFlowHook(store, trigger, now, dir);
|
|
745
|
+
return renewed.ok ? settle(renewed.said, { reveal: renewed.reveal }) : answer(409, { ok: false, said: renewed.message });
|
|
746
|
+
}
|
|
747
|
+
// Linear's signing secret, pasted on this secure panel behind the password — never in chat.
|
|
748
|
+
if (!authenticateApprover(who, body.get("password") ?? "").ok)
|
|
749
|
+
return answer(403, { ok: false, said: "Enter your Toolroll password to save a secret." });
|
|
750
|
+
const saved = saveLinearSigningSecret(trigger, body.get("secret") ?? "", dir);
|
|
751
|
+
return saved.ok ? settle("Signing secret saved. Linear's deliveries can be proved now.") : answer(400, { ok: false, said: saved.message });
|
|
752
|
+
};
|
|
753
|
+
const recipeAct = (step) => async (ctx) => {
|
|
754
|
+
const { who, request, response, now, posted } = ctx;
|
|
755
|
+
const body = readForm(posted, CONSOLE_FORMS.recipes);
|
|
756
|
+
const project = projectOf(who, request);
|
|
757
|
+
try {
|
|
758
|
+
if (project === null || project === undefined || !visible(project))
|
|
759
|
+
throw new RecipeError("Open a project first.", 403);
|
|
760
|
+
for (const field of ["repo", "projectRevision", "preview", "source", "sourceRevision", "document", "recipe", "recipeRevision", "purpose", "next"])
|
|
761
|
+
if (body.getAll(field).length > 1)
|
|
762
|
+
throw new RecipeError(`Duplicated ${field} field.`);
|
|
763
|
+
if (body.get("repo") !== project || (who.via === "cookie" && body.get("projectRevision") !== String(who.session.projectRevision)))
|
|
764
|
+
throw new RecipeError("The open project changed. Reopen this recipe in the intended project.", 409);
|
|
765
|
+
const rootMode = !unscopedMode && ceiling.roots.length > 0;
|
|
766
|
+
if (rootMode && !(await authorizedProject(liveCeiling(), project)))
|
|
767
|
+
throw new RecipeError("The project is outside this server's access.", 403);
|
|
768
|
+
const rechecked = authorizeMutation(request, who, readForm(posted, CONSOLE_FORMS.mutationGuard));
|
|
769
|
+
if (rechecked !== null)
|
|
770
|
+
throw new RecipeError(rechecked.message, rechecked.status);
|
|
771
|
+
if (projectOf(who, request) !== project)
|
|
772
|
+
throw new RecipeError("The open project changed. Preview the intended project again.", 409);
|
|
773
|
+
const revision = who.via === "cookie" ? who.session.projectRevision : 0;
|
|
774
|
+
if (step === "prepare") {
|
|
775
|
+
const id = body.get("recipe") ?? "", version = body.get("recipeRevision") ?? "";
|
|
776
|
+
if (!/^[1-9][0-9]{0,8}$/.test(version))
|
|
777
|
+
throw new RecipeError("Choose a valid recipe version.");
|
|
778
|
+
const recipe = findRecipe(store, who.name, project, id, Number(version));
|
|
779
|
+
if (recipe === null || recipe.repo === null)
|
|
780
|
+
throw new RecipeError("This recipe is not available in this project.", 404);
|
|
449
781
|
try {
|
|
450
|
-
|
|
782
|
+
const preview = prepareRecipeRun(store, who.name, project, id, Number(version), recipeAnswersFromForm(body.sent), now);
|
|
783
|
+
return redirect(response, `/recipes/preview?preview=${preview.token}`);
|
|
451
784
|
}
|
|
452
785
|
catch (error) {
|
|
453
|
-
if (error instanceof
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
}
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
const
|
|
471
|
-
if (!
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
const name = (body.get("name") ?? "").replace(/[\u0000-\u001f\u007f]/g, "").trim().slice(0, 80) || template.label;
|
|
475
|
-
const made = store.createFlow({ repo, name, definitionJson: JSON.stringify(template.definition), by: who.name }, now);
|
|
476
|
-
// A template that starts from something (Issues to PRs: labelled issues) comes with its trigger.
|
|
477
|
-
const trigger = template.trigger === undefined ? null : addFlowTriggerTo(store, store.getFlow(made), template.trigger, who.name, now, options.configDir ?? null);
|
|
478
|
-
if (trigger !== null && !trigger.ok)
|
|
479
|
-
return redirect(response, `/flows?problem=${encodeURIComponent(`${name} was made, but its trigger wasn't: ${trigger.message} Add one on its Triggers panel.`)}`);
|
|
480
|
-
return redirect(response, `/flows/${made}`);
|
|
481
|
-
}
|
|
482
|
-
const flow = store.getFlow(Number((flowPost ?? triggerPost)[1]));
|
|
483
|
-
if (flow === null || flow.state !== "active" || !visible(flow.repo))
|
|
484
|
-
return answer(404, { ok: false, said: "No such flow in your projects." });
|
|
485
|
-
const definition = flowDefinitionOf(flow);
|
|
486
|
-
const dir = options.configDir ?? null;
|
|
487
|
-
const viewNow = () => flowView(store, store.getFlow(flow.id) ?? flow, { name: who.name, approver: true }, null, { dir, repos: projects, sortReady: keyStatus("openrouter", providerHome).set, toolHome });
|
|
488
|
-
const settle = (said, extra = {}) => {
|
|
489
|
-
// Move what can move right away (a message, a decision's next zone); workers file and run the tasks.
|
|
490
|
-
try {
|
|
491
|
-
advanceFlows(store, flow.repo, now, { evidenceRoot });
|
|
492
|
-
}
|
|
493
|
-
catch { /* the next worker pass retries */ }
|
|
494
|
-
return answer(200, { ok: true, said, view: viewNow(), ...extra });
|
|
495
|
-
};
|
|
496
|
-
if (triggerPost !== null) {
|
|
497
|
-
const trigger = store.getFlowTrigger(Number(triggerPost[2]));
|
|
498
|
-
if (trigger === null || trigger.flow !== flow.id || trigger.state === "removed")
|
|
499
|
-
return answer(404, { ok: false, said: "That trigger isn't on this flow." });
|
|
500
|
-
const verb = triggerPost[3];
|
|
501
|
-
if (verb === "pause" || verb === "resume") {
|
|
502
|
-
setFlowTriggerOn(store, trigger, verb === "resume", now);
|
|
503
|
-
return settle(verb === "pause" ? "Trigger paused." : "Trigger on again.");
|
|
504
|
-
}
|
|
505
|
-
if (verb === "remove") {
|
|
506
|
-
removeFlowTrigger(store, trigger, now, dir);
|
|
507
|
-
return settle("Trigger removed.");
|
|
508
|
-
}
|
|
509
|
-
if (verb === "share") {
|
|
510
|
-
const shared = shareFlowButton(store, trigger, now, dir);
|
|
511
|
-
return shared.ok ? settle(shared.said, { reveal: shared.reveal }) : answer(409, { ok: false, said: shared.message });
|
|
512
|
-
}
|
|
513
|
-
if (verb === "unshare") {
|
|
514
|
-
stopSharingFlowButton(store, trigger, now);
|
|
515
|
-
return settle("The form link no longer works.");
|
|
516
|
-
}
|
|
517
|
-
if (verb === "press") {
|
|
518
|
-
let answers;
|
|
519
|
-
try {
|
|
520
|
-
answers = JSON.parse(body.get("answers") ?? "[]");
|
|
521
|
-
}
|
|
522
|
-
catch {
|
|
523
|
-
answers = [];
|
|
524
|
-
}
|
|
525
|
-
const pressed = pressFlowButton(store, trigger, Array.isArray(answers) ? answers : [], who.name, now);
|
|
526
|
-
return pressed.ok ? settle(pressed.said) : answer(400, { ok: false, said: pressed.message });
|
|
527
|
-
}
|
|
528
|
-
if (verb === "check") {
|
|
529
|
-
const checked = await checkFlowTriggerNow(store, trigger, now, { gh: options.flowTriggerIo?.gh ?? execRun, fetch: options.flowTriggerIo?.fetch ?? fetch, dir, ...(options.flowTriggerIo?.mail === undefined ? {} : { mail: options.flowTriggerIo.mail }),
|
|
530
|
-
// A schedule's script, run now (v90): in a clean folder beside the database, inside the agents' fence.
|
|
531
|
-
shell: options.flowTriggerIo?.shell ?? execRun, scratch: options.flowTriggerIo?.scratch ?? join(dir ?? tmpdir(), "flow-scratch") });
|
|
532
|
-
try {
|
|
533
|
-
advanceFlows(store, flow.repo, now, { evidenceRoot });
|
|
534
|
-
}
|
|
535
|
-
catch { /* the next worker pass retries */ }
|
|
536
|
-
return answer(checked.ok ? 200 : 409, { ok: checked.ok, said: checked.said, view: viewNow() });
|
|
537
|
-
}
|
|
538
|
-
if (dir === null)
|
|
539
|
-
return answer(409, { ok: false, said: "Webhook secrets are kept on the console's computer." });
|
|
540
|
-
if (verb === "renew") {
|
|
541
|
-
const renewed = renewFlowHook(store, trigger, now, dir);
|
|
542
|
-
return renewed.ok ? settle(renewed.said, { reveal: renewed.reveal }) : answer(409, { ok: false, said: renewed.message });
|
|
543
|
-
}
|
|
544
|
-
// Linear's signing secret, pasted on this secure panel behind the password — never in chat.
|
|
545
|
-
if (!authenticateApprover(who, body.get("password") ?? "").ok)
|
|
546
|
-
return answer(403, { ok: false, said: "Enter your Toolroll password to save a secret." });
|
|
547
|
-
const saved = saveLinearSigningSecret(trigger, body.get("secret") ?? "", dir);
|
|
548
|
-
return saved.ok ? settle("Signing secret saved. Linear's deliveries can be proved now.") : answer(400, { ok: false, said: saved.message });
|
|
549
|
-
}
|
|
550
|
-
const action = flowPost[2];
|
|
551
|
-
if (action === "triggers") {
|
|
552
|
-
let raw;
|
|
553
|
-
try {
|
|
554
|
-
raw = JSON.parse(body.get("trigger") ?? "null");
|
|
555
|
-
}
|
|
556
|
-
catch {
|
|
557
|
-
return answer(400, { ok: false, said: "That trigger couldn't be read." });
|
|
558
|
-
}
|
|
559
|
-
const made = addFlowTriggerTo(store, flow, raw, who.name, now, dir);
|
|
560
|
-
return made.ok ? settle(made.said, made.reveal === null ? {} : { reveal: made.reveal }) : answer(400, { ok: false, said: made.message });
|
|
561
|
-
}
|
|
562
|
-
if (action === "scripts") {
|
|
563
|
-
// The project's script library, reached from any of its flows.
|
|
564
|
-
if (body.get("approve") === "yes")
|
|
565
|
-
return store.approveFlowScript(flow.repo, body.get("name") ?? "") ? settle("Approved. Zones that run it go on.") : answer(404, { ok: false, said: "That script isn't waiting for approval." });
|
|
566
|
-
if (body.get("remove") === "yes")
|
|
567
|
-
return store.removeFlowScript(flow.repo, body.get("name") ?? "") ? settle("Script removed. Zones that ran it wait until it's back.") : answer(404, { ok: false, said: "There's no script by that name." });
|
|
568
|
-
const saved = saveScript(store, flow.repo, { name: body.get("name"), about: body.get("about"), body: body.get("body"), timeoutMinutes: body.get("timeoutMinutes"), language: body.get("language"), file: body.get("file") }, who.name, now);
|
|
569
|
-
return saved.ok ? settle(saved.said) : answer(400, { ok: false, said: saved.message });
|
|
570
|
-
}
|
|
571
|
-
if (action === "linear-key" || action === "hooks-address") {
|
|
572
|
-
// Installation settings, set from the flow they are needed on.
|
|
573
|
-
if (dir === null)
|
|
574
|
-
return answer(409, { ok: false, said: "These settings are kept on the console's computer." });
|
|
575
|
-
if (action === "hooks-address") {
|
|
576
|
-
const saved = saveHooksBase(dir, body.get("address") ?? "");
|
|
577
|
-
return saved.ok ? settle(saved.base === null ? "Public address cleared." : "Public address saved.") : answer(400, { ok: false, said: saved.message });
|
|
578
|
-
}
|
|
579
|
-
if (body.get("remove") === "yes") {
|
|
580
|
-
removeLinearKey(dir);
|
|
581
|
-
return settle("Linear key removed.");
|
|
582
|
-
}
|
|
583
|
-
if (!authenticateApprover(who, body.get("password") ?? "").ok)
|
|
584
|
-
return answer(403, { ok: false, said: "Enter your Toolroll password to save a key." });
|
|
585
|
-
const saved = saveLinearKey(dir, body.get("key") ?? "");
|
|
586
|
-
return saved.ok ? settle("Linear key saved on this computer.") : answer(400, { ok: false, said: saved.message });
|
|
587
|
-
}
|
|
588
|
-
if (action === "archive") {
|
|
589
|
-
store.archiveFlow(flow.id, who.name, now);
|
|
590
|
-
return redirect(response, "/flows");
|
|
591
|
-
}
|
|
592
|
-
if (action === "save") {
|
|
593
|
-
let saved, drawn;
|
|
786
|
+
if (!(error instanceof RecipeError))
|
|
787
|
+
throw error;
|
|
788
|
+
return sendScreen(response, error.status, screen("Use recipe", recipeRunHtml(recipe, project, revision, error.message, body.sent), { chrome: chromeFor(project, "recipes"), functional: { script: recipeScript() } }));
|
|
789
|
+
}
|
|
790
|
+
}
|
|
791
|
+
if (step === "import") {
|
|
792
|
+
const document = importRecipe(body.get("document") ?? "");
|
|
793
|
+
return sendScreen(response, 200, screen("Imported recipe", recipeEditorHtml({ id: "imported", revision: 1, repo: project, document, digest: "", author: who.name }, project, revision, null, undefined, true), { chrome: chromeFor(project, "recipes"), functional: { script: recipeScript() } }));
|
|
794
|
+
}
|
|
795
|
+
if (step === "preview") {
|
|
796
|
+
const purpose = body.get("purpose") ?? "workflow";
|
|
797
|
+
if (!["recipe", "workflow"].includes(purpose))
|
|
798
|
+
throw new RecipeError("Choose preview recipe or preview workflow.");
|
|
799
|
+
const source = body.get("source") ?? "custom";
|
|
800
|
+
const sourceVersion = body.get("sourceRevision") ?? "1";
|
|
801
|
+
if (!/^[1-9][0-9]{0,8}$/.test(sourceVersion))
|
|
802
|
+
throw new RecipeError("Choose a valid recipe version.");
|
|
803
|
+
const original = ["custom", "imported", "task-copy"].includes(source) ? null : findRecipe(store, who.name, project, source, Number(sourceVersion));
|
|
804
|
+
if (original === null && !["custom", "imported", "task-copy"].includes(source))
|
|
805
|
+
throw new RecipeError("That recipe version is not available in this project.", 404);
|
|
806
|
+
let document;
|
|
594
807
|
try {
|
|
595
|
-
|
|
596
|
-
saved = validateFlowDefinition(drawn);
|
|
597
|
-
// A {{stage.…}} this save adds must be one its zone hands on; ones the saved flow had stay.
|
|
598
|
-
const references = stageReferenceProblems(saved, flowDefinitionOf(flow));
|
|
599
|
-
if (references.length > 0)
|
|
600
|
-
throw new FlowContractError(references.map(one => one.line));
|
|
808
|
+
document = recipeFromForm(body.sent);
|
|
601
809
|
}
|
|
602
810
|
catch (error) {
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
}
|
|
627
|
-
if (definition === null)
|
|
628
|
-
return answer(409, { ok: false, said: "This flow's drawing can't be read. Save it again from the editor." });
|
|
629
|
-
if (action === "cards") {
|
|
630
|
-
const added = addCardToFlow(store, flow, { title: body.get("title"), description: body.get("description"), stage: body.get("stage") }, who.name, now);
|
|
631
|
-
return added.ok ? settle("Card added.") : answer(400, { ok: false, said: added.message });
|
|
632
|
-
}
|
|
633
|
-
const target = store.getFlowCard(Number(flowPost[2]));
|
|
634
|
-
if (target === null || target.flow !== flow.id)
|
|
635
|
-
return answer(404, { ok: false, said: "That card isn't in this flow." });
|
|
636
|
-
const verb = flowPost[3];
|
|
637
|
-
if (verb === "move") {
|
|
638
|
-
if (!definition.stages.some(one => one.id === body.get("stage")))
|
|
639
|
-
return answer(400, { ok: false, said: "Choose a zone in this flow." });
|
|
640
|
-
const moved = moveCardInFlow(store, target, body.get("stage") ?? "", who.name, now);
|
|
641
|
-
return !moved.ok ? answer(409, { ok: false, said: moved.message }) : moved.said === "Already there." ? answer(200, { ok: true, said: moved.said }) : settle(moved.said);
|
|
642
|
-
}
|
|
643
|
-
if (verb === "decide") {
|
|
644
|
-
const decision = body.get("decision") === "approve" ? "approve" : body.get("decision") === "send-back" ? "send-back" : null;
|
|
645
|
-
if (decision === null)
|
|
646
|
-
return answer(400, { ok: false, said: "Approve it or send it back." });
|
|
647
|
-
const note = (body.get("note") ?? "").trim() || null;
|
|
648
|
-
if (note !== null && note.length > LIMITS.note)
|
|
649
|
-
return answer(400, { ok: false, said: `Keep the note to ${LIMITS.note.toLocaleString("en-US")} characters; this is ${note.length.toLocaleString("en-US")}.` });
|
|
650
|
-
const decided = decideFlowCard(store, { card: target.id, decision, note, actor: who.name, repos: projects, evidenceRoot, draft: body.get("draft"), where: "the console" }, now);
|
|
651
|
-
return decided.ok ? settle(decided.said) : answer(409, { ok: false, said: decided.message });
|
|
652
|
-
}
|
|
653
|
-
if (verb === "choose") {
|
|
654
|
-
// "Person chooses": one of the zone's options (by its place and words), or a reply that becomes the note.
|
|
655
|
-
const choice = /^[0-9]$/.test(body.get("choice") ?? "") ? Number(body.get("choice")) : null;
|
|
656
|
-
const chosen = chooseFlowCard(store, { card: target.id, ...(/^[1-9][0-9]{0,9}$/.test(body.get("entry") ?? "") ? { entry: Number(body.get("entry")) } : {}), choice,
|
|
657
|
-
...(choice !== null && body.get("label") !== null ? { label: body.get("label") } : {}), note: choice === null ? body.get("note") : null, actor: who.name, where: "Toolroll", repos: projects, evidenceRoot }, now);
|
|
658
|
-
return chosen.ok ? settle(chosen.said) : answer(409, { ok: false, said: chosen.message });
|
|
659
|
-
}
|
|
660
|
-
if (verb === "comment") {
|
|
661
|
-
const commented = commentOnFlowCard(store, target, who.name, body.get("body"), now);
|
|
662
|
-
return commented.ok ? settle(commented.said) : answer(400, { ok: false, said: commented.message });
|
|
663
|
-
}
|
|
664
|
-
if (verb === "assign") {
|
|
665
|
-
const assigned = assignFlowCard(store, target, body.get("owner") || null, who.name, now);
|
|
666
|
-
return assigned.ok ? settle(assigned.said) : answer(400, { ok: false, said: assigned.message });
|
|
667
|
-
}
|
|
668
|
-
if (verb === "watch")
|
|
669
|
-
return settle(watchFlowCard(store, target, who.name, body.get("watching") !== "no", now).ok ? body.get("watching") === "no" ? "You won't hear about this card unless someone mentions you." : "You'll hear about this card." : "Done.");
|
|
670
|
-
if (verb === "cancel") {
|
|
671
|
-
const cancelled = cancelFlowCard(store, target, who.name, now);
|
|
672
|
-
return cancelled.ok ? settle(cancelled.said) : answer(409, { ok: false, said: cancelled.message });
|
|
673
|
-
}
|
|
674
|
-
return answer(404, { ok: false, said: "No such flow action." });
|
|
675
|
-
}
|
|
676
|
-
if (["/recipes/prepare", "/recipes/preview", "/recipes/import", "/recipes/launch", "/recipes/save"].includes(url.pathname)) {
|
|
677
|
-
const body = readForm(posted, CONSOLE_FORMS.recipes);
|
|
678
|
-
const project = projectOf(who, request);
|
|
679
|
-
try {
|
|
680
|
-
if (project === null || project === undefined || !visible(project))
|
|
681
|
-
throw new RecipeError("Open a project first.", 403);
|
|
682
|
-
for (const field of ["repo", "projectRevision", "preview", "source", "sourceRevision", "document", "recipe", "recipeRevision", "purpose", "next"])
|
|
683
|
-
if (body.getAll(field).length > 1)
|
|
684
|
-
throw new RecipeError(`Duplicated ${field} field.`);
|
|
685
|
-
if (body.get("repo") !== project || (who.via === "cookie" && body.get("projectRevision") !== String(who.session.projectRevision)))
|
|
686
|
-
throw new RecipeError("The open project changed. Reopen this recipe in the intended project.", 409);
|
|
687
|
-
const rootMode = !unscopedMode && ceiling.roots.length > 0;
|
|
688
|
-
if (rootMode && !(await authorizedProject(liveCeiling(), project)))
|
|
689
|
-
throw new RecipeError("The project is outside this server's access.", 403);
|
|
690
|
-
const rechecked = authorizeMutation(request, who, readForm(posted, CONSOLE_FORMS.mutationGuard));
|
|
691
|
-
if (rechecked !== null)
|
|
692
|
-
throw new RecipeError(rechecked.message, rechecked.status);
|
|
693
|
-
if (projectOf(who, request) !== project)
|
|
694
|
-
throw new RecipeError("The open project changed. Preview the intended project again.", 409);
|
|
695
|
-
const csrf = who.via === "cookie" ? who.session.csrf : "";
|
|
696
|
-
const revision = who.via === "cookie" ? who.session.projectRevision : 0;
|
|
697
|
-
if (url.pathname === "/recipes/prepare") {
|
|
698
|
-
const id = body.get("recipe") ?? "", version = body.get("recipeRevision") ?? "";
|
|
699
|
-
if (!/^[1-9][0-9]{0,8}$/.test(version))
|
|
700
|
-
throw new RecipeError("Choose a valid recipe version.");
|
|
701
|
-
const recipe = findRecipe(store, who.name, project, id, Number(version));
|
|
702
|
-
if (recipe === null || recipe.repo === null)
|
|
703
|
-
throw new RecipeError("This recipe is not available in this project.", 404);
|
|
704
|
-
try {
|
|
705
|
-
const preview = prepareRecipeRun(store, who.name, project, id, Number(version), recipeAnswersFromForm(body.sent), now);
|
|
706
|
-
return redirect(response, `/recipes/preview?preview=${preview.token}`);
|
|
707
|
-
}
|
|
708
|
-
catch (error) {
|
|
709
|
-
if (!(error instanceof RecipeError))
|
|
710
|
-
throw error;
|
|
711
|
-
return sendScreen(response, error.status, screen("Use recipe", recipeRunHtml(recipe, project, csrf, revision, error.message, body.sent), { chrome: chromeFor(project, "recipes"), functional: { script: recipeScript() } }));
|
|
712
|
-
}
|
|
713
|
-
}
|
|
714
|
-
if (url.pathname === "/recipes/import") {
|
|
715
|
-
const document = importRecipe(body.get("document") ?? "");
|
|
716
|
-
return sendScreen(response, 200, screen("Imported recipe", recipeEditorHtml({ id: "imported", revision: 1, repo: project, document, digest: "", author: who.name }, project, csrf, revision, null, undefined, true), { chrome: chromeFor(project, "recipes"), functional: { script: recipeScript() } }));
|
|
717
|
-
}
|
|
718
|
-
if (url.pathname === "/recipes/preview") {
|
|
719
|
-
const purpose = body.get("purpose") ?? "workflow";
|
|
720
|
-
if (!["recipe", "workflow"].includes(purpose))
|
|
721
|
-
throw new RecipeError("Choose preview recipe or preview workflow.");
|
|
722
|
-
const source = body.get("source") ?? "custom";
|
|
723
|
-
const sourceVersion = body.get("sourceRevision") ?? "1";
|
|
724
|
-
if (!/^[1-9][0-9]{0,8}$/.test(sourceVersion))
|
|
725
|
-
throw new RecipeError("Choose a valid recipe version.");
|
|
726
|
-
const original = ["custom", "imported", "task-copy"].includes(source) ? null : findRecipe(store, who.name, project, source, Number(sourceVersion));
|
|
727
|
-
if (original === null && !["custom", "imported", "task-copy"].includes(source))
|
|
728
|
-
throw new RecipeError("That recipe version is not available in this project.", 404);
|
|
729
|
-
let document;
|
|
730
|
-
try {
|
|
731
|
-
document = recipeFromForm(body.sent);
|
|
732
|
-
}
|
|
733
|
-
catch (error) {
|
|
734
|
-
if (!(error instanceof RecipeError))
|
|
735
|
-
throw error;
|
|
736
|
-
const fallback = original ?? { ...starterRecipes().find(one => one.id === "small-feature"), id: source };
|
|
737
|
-
return sendScreen(response, error.status, screen("Customize recipe", recipeEditorHtml(fallback, project, csrf, revision, error.message, body.sent, purpose === "recipe"), { chrome: chromeFor(project, "recipes"), functional: { script: recipeScript() } }));
|
|
738
|
-
}
|
|
739
|
-
const preview = createWorkflowPreview(store, who.name, project, document, source, now);
|
|
740
|
-
return redirect(response, `/recipes/preview?preview=${preview.token}${purpose === "recipe" || document.version === 2 ? "&purpose=recipe" : ""}`);
|
|
741
|
-
}
|
|
742
|
-
const token = body.get("preview") ?? "";
|
|
743
|
-
if (url.pathname === "/recipes/save") {
|
|
744
|
-
if (body.has("next") && body.get("next") !== "use")
|
|
745
|
-
throw new RecipeError("Choose a valid recipe destination.");
|
|
746
|
-
const saved = saveWorkflowRecipe(store, who.name, project, token, now);
|
|
747
|
-
return redirect(response, body.get("next") === "use" ? `/recipes/run?recipe=${saved.id}&revision=${saved.revision}&saved=1` : `/recipes/preview?preview=${token}`);
|
|
748
|
-
}
|
|
749
|
-
const made = launchWorkflow(store, who.name, project, token, now, who.via === "cookie");
|
|
750
|
-
if (rootMode)
|
|
751
|
-
store.upsertProject(project, projectName(project), now);
|
|
752
|
-
if (made.taskId !== null) {
|
|
753
|
-
const context = requestContext.getStore();
|
|
754
|
-
if (context !== undefined)
|
|
755
|
-
context.createdTask = made.taskId;
|
|
756
|
-
}
|
|
757
|
-
return redirect(response, made.taskId === null ? `/flows/${made.flowId}` : taskHref(made.taskId));
|
|
758
|
-
}
|
|
759
|
-
catch (error) {
|
|
760
|
-
if (error instanceof RecipeError)
|
|
761
|
-
return refuse(response, who, error.status, error.message, "/recipes");
|
|
762
|
-
throw error;
|
|
763
|
-
}
|
|
764
|
-
}
|
|
765
|
-
return refuse(response, who, 404, "There's no page at this address.", "/chat");
|
|
766
|
-
}
|
|
767
|
-
// ---- reads ---------------------------------------------------------------
|
|
768
|
-
/** A gallery template's page (Flows → New → Use this): its questions, the preview of these answers, and Create. */
|
|
769
|
-
function sendGalleryUse(response, who, template, projects, repo, given, name, notice, said = null) {
|
|
770
|
-
const answers = given ?? galleryDefaults(store, template, repo);
|
|
771
|
-
let preview = null, problem = notice;
|
|
772
|
-
try {
|
|
773
|
-
preview = previewGallery(store, template, repo, answers, who.name, clock());
|
|
811
|
+
if (!(error instanceof RecipeError))
|
|
812
|
+
throw error;
|
|
813
|
+
const fallback = original ?? { ...starterRecipes().find(one => one.id === "small-feature"), id: source };
|
|
814
|
+
return sendScreen(response, error.status, screen("Customize recipe", recipeEditorHtml(fallback, project, revision, error.message, body.sent, purpose === "recipe"), { chrome: chromeFor(project, "recipes"), functional: { script: recipeScript() } }));
|
|
815
|
+
}
|
|
816
|
+
const preview = createWorkflowPreview(store, who.name, project, document, source, now);
|
|
817
|
+
return redirect(response, `/recipes/preview?preview=${preview.token}${purpose === "recipe" || document.version === 2 ? "&purpose=recipe" : ""}`);
|
|
818
|
+
}
|
|
819
|
+
const token = body.get("preview") ?? "";
|
|
820
|
+
if (step === "save") {
|
|
821
|
+
if (body.has("next") && body.get("next") !== "use")
|
|
822
|
+
throw new RecipeError("Choose a valid recipe destination.");
|
|
823
|
+
const saved = saveWorkflowRecipe(store, who.name, project, token, now);
|
|
824
|
+
return redirect(response, body.get("next") === "use" ? `/recipes/run?recipe=${saved.id}&revision=${saved.revision}&saved=1` : `/recipes/preview?preview=${token}`);
|
|
825
|
+
}
|
|
826
|
+
const made = launchWorkflow(store, who.name, project, token, now, who.via === "cookie");
|
|
827
|
+
if (rootMode)
|
|
828
|
+
store.upsertProject(project, projectName(project), now);
|
|
829
|
+
if (made.taskId !== null) {
|
|
830
|
+
const context = requestContext.getStore();
|
|
831
|
+
if (context !== undefined)
|
|
832
|
+
context.createdTask = made.taskId;
|
|
833
|
+
}
|
|
834
|
+
return redirect(response, made.taskId === null ? `/flows/${made.flowId}` : taskHref(made.taskId));
|
|
774
835
|
}
|
|
775
836
|
catch (error) {
|
|
776
|
-
|
|
837
|
+
if (error instanceof RecipeError)
|
|
838
|
+
return refuse(response, who, error.status, error.message, "/recipes");
|
|
839
|
+
throw error;
|
|
777
840
|
}
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
841
|
+
};
|
|
842
|
+
const registrations = handlersOf("flows", {
|
|
843
|
+
"flows.new": newFlowGallery,
|
|
844
|
+
"flows.gallery": galleryTemplatePage,
|
|
845
|
+
"flows.page": flowsPage,
|
|
846
|
+
"kits": kitsGallery,
|
|
847
|
+
"kit.page": kitPage,
|
|
848
|
+
"subagents": subagentsPage,
|
|
849
|
+
"subagents.legacy": legacySubagentLink,
|
|
850
|
+
"subagent.page": subagentPage,
|
|
851
|
+
"subagent.soul-file": subagentSoulFile,
|
|
852
|
+
"flow.insights": flowInsightsRead,
|
|
853
|
+
"flow.run": flowRunRead,
|
|
854
|
+
"flow.export": flowExportFile,
|
|
855
|
+
"flow.page": flowPage,
|
|
856
|
+
"recipes": recipeLibrary,
|
|
857
|
+
"recipes.run": recipeScreen("run"),
|
|
858
|
+
"recipes.start": recipeScreen("start"),
|
|
859
|
+
"recipes.new": recipeScreen("new"),
|
|
860
|
+
"recipes.edit": recipeScreen("edit"),
|
|
861
|
+
"recipes.from-task": recipeScreen("from-task"),
|
|
862
|
+
"recipes.preview": recipeScreen("preview"),
|
|
863
|
+
"recipes.export": recipeScreen("export"),
|
|
864
|
+
"recipes.other": recipeScreen("other"),
|
|
865
|
+
"kit.setup": kitAct("setup"),
|
|
866
|
+
"kit.sample": kitAct("sample"),
|
|
867
|
+
"kit.github": kitAct("github"),
|
|
868
|
+
"subagent.new": newSubagent,
|
|
869
|
+
"subagent.soul": subagentAct("soul"),
|
|
870
|
+
"subagent.state": subagentAct("state"),
|
|
871
|
+
"subagent.note": subagentAct("note"),
|
|
872
|
+
"subagent.settings": subagentAct("settings"),
|
|
873
|
+
"subagent.summary": subagentAct("summary"),
|
|
874
|
+
"subagent.tools": subagentAct("tools"),
|
|
875
|
+
"subagent.memory": subagentAct("memory"),
|
|
876
|
+
"subagent.routines": subagentAct("routines"),
|
|
877
|
+
"subagent.week": subagentAct("week"),
|
|
878
|
+
"subagent.answer": answerSubagentQuestionSend,
|
|
879
|
+
"flows.gallery-create": createFromGallery,
|
|
880
|
+
"flows.create": createFlow,
|
|
881
|
+
"flows.example": createExampleFlow,
|
|
882
|
+
"flows.import": importFlowFile,
|
|
883
|
+
"flow.save": flowAct("save"),
|
|
884
|
+
"flow.cards": flowAct("cards"),
|
|
885
|
+
"flow.archive": flowAct("archive"),
|
|
886
|
+
"flow.scripts": flowAct("scripts"),
|
|
887
|
+
"flow.linear-key": flowAct("linear-key"),
|
|
888
|
+
"flow.hooks-address": flowAct("hooks-address"),
|
|
889
|
+
"flow.secrets": flowAct("secrets"),
|
|
890
|
+
"flow.triggers": flowAct("triggers"),
|
|
891
|
+
"flow.card.move": cardAct("move"),
|
|
892
|
+
"flow.card.decide": cardAct("decide"),
|
|
893
|
+
"flow.card.cancel": cardAct("cancel"),
|
|
894
|
+
"flow.card.comment": cardAct("comment"),
|
|
895
|
+
"flow.card.assign": cardAct("assign"),
|
|
896
|
+
"flow.card.watch": cardAct("watch"),
|
|
897
|
+
"flow.card.choose": cardAct("choose"),
|
|
898
|
+
"flow.trigger.pause": triggerAct("pause"),
|
|
899
|
+
"flow.trigger.resume": triggerAct("resume"),
|
|
900
|
+
"flow.trigger.remove": triggerAct("remove"),
|
|
901
|
+
"flow.trigger.check": triggerAct("check"),
|
|
902
|
+
"flow.trigger.press": triggerAct("press"),
|
|
903
|
+
"flow.trigger.renew": triggerAct("renew"),
|
|
904
|
+
"flow.trigger.secret": triggerAct("secret"),
|
|
905
|
+
"flow.trigger.share": triggerAct("share"),
|
|
906
|
+
"flow.trigger.unshare": triggerAct("unshare"),
|
|
907
|
+
"recipes.prepare-send": recipeAct("prepare"),
|
|
908
|
+
"recipes.preview-send": recipeAct("preview"),
|
|
909
|
+
"recipes.import-send": recipeAct("import"),
|
|
910
|
+
"recipes.save-send": recipeAct("save"),
|
|
911
|
+
"recipes.launch-send": recipeAct("launch"),
|
|
912
|
+
});
|
|
913
|
+
return { registrations, sendGalleryUse };
|
|
833
914
|
}
|