toolroll 0.9.48 → 0.9.50
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 +17 -0
- package/dist/cli-http.js +3 -0
- package/dist/desktop-update.d.ts +2 -2
- package/dist/flow-triggers.js +7 -0
- package/dist/mcp-http.d.ts +1 -0
- package/dist/mcp-http.js +12 -15
- package/dist/mcp-oauth.d.ts +3 -0
- package/dist/mcp-oauth.js +13 -21
- package/dist/mcp-person.d.ts +0 -9
- package/dist/mcp-person.js +2 -1
- package/dist/operate.js +4 -1
- package/dist/public-access.d.ts +3 -1
- package/dist/public-access.js +5 -1
- package/dist/request-budget.d.ts +30 -3
- package/dist/request-budget.js +44 -1
- package/dist/serve.d.ts +2 -605
- package/dist/serve.js +462 -23733
- package/dist/server/chat.d.ts +41 -0
- package/dist/server/chat.js +1458 -0
- package/dist/server/edge-refusal.d.ts +5 -0
- package/dist/server/edge-refusal.js +56 -0
- package/dist/server/flows.d.ts +12 -0
- package/dist/server/flows.js +832 -0
- package/dist/server/guards.d.ts +72 -0
- package/dist/server/guards.js +357 -0
- package/dist/server/handler-context.d.ts +22 -0
- package/dist/server/handler-context.js +1 -0
- package/dist/server/handler-registry.d.ts +21 -0
- package/dist/server/handler-registry.js +34 -0
- package/dist/server/pages-assets.d.ts +10 -0
- package/dist/server/pages-assets.js +103 -0
- package/dist/server/pages.d.ts +11 -0
- package/dist/server/pages.js +910 -0
- package/dist/server/people-tokens.d.ts +20 -0
- package/dist/server/people-tokens.js +694 -0
- package/dist/server/remote-hooks.d.ts +29 -0
- package/dist/server/remote-hooks.js +130 -0
- package/dist/server/remote.d.ts +59 -0
- package/dist/server/remote.js +185 -0
- package/dist/server/route-policy.d.ts +18 -0
- package/dist/server/route-policy.js +30 -0
- package/dist/server/route-table.d.ts +74 -0
- package/dist/server/route-table.js +350 -0
- package/dist/server/runtime.d.ts +280 -0
- package/dist/server/runtime.js +25 -0
- package/dist/server/settings.d.ts +22 -0
- package/dist/server/settings.js +2388 -0
- package/dist/server/shared.d.ts +3334 -0
- package/dist/server/shared.js +12708 -0
- package/dist/server/task-views.d.ts +82 -0
- package/dist/server/task-views.js +1099 -0
- package/dist/server/tasks.d.ts +87 -0
- package/dist/server/tasks.js +3579 -0
- package/dist/session-http.d.ts +5 -0
- package/dist/session-http.js +16 -0
- package/dist/session-server.d.ts +5 -0
- package/dist/session-server.js +2 -0
- package/dist/team-cli.js +28 -6
- package/dist/team-contract.d.ts +7 -0
- package/dist/team-contract.js +7 -0
- package/dist/team-http.d.ts +10 -1
- package/dist/team-http.js +23 -3
- package/dist/team-leads.d.ts +5 -1
- package/dist/team-leads.js +42 -4
- package/dist/team-runtime.d.ts +1 -1
- package/dist/team-runtime.js +3 -0
- package/dist/teams.d.ts +3 -0
- package/dist/teams.js +18 -0
- package/package.json +1 -1
|
@@ -0,0 +1,832 @@
|
|
|
1
|
+
/** flows handlers, moved without changing their route bodies. */
|
|
2
|
+
import { createHash } from "node:crypto";
|
|
3
|
+
import {} from "node:http";
|
|
4
|
+
import { tmpdir } from "node:os";
|
|
5
|
+
import { join } from "node:path";
|
|
6
|
+
import { resolveRoutineAuthority } from "../agentconfig.js";
|
|
7
|
+
import { checkResponse, CONSOLE_FORMS, readForm } from "../contracts/console-api.js";
|
|
8
|
+
import { stageReferenceProblems } from "../contracts/stage-output.js";
|
|
9
|
+
import { LIMITS } from "../decision.js";
|
|
10
|
+
import { run as execRun } from "../exec.js";
|
|
11
|
+
import { setFlowSecret } from "../flow-actions.js";
|
|
12
|
+
import { addCardToFlow, advanceFlows, cancelFlowCard, crossProjectProblem, decideFlowCard, flowDefinitionOf, moveCardInFlow } from "../flow-engine.js";
|
|
13
|
+
import { galleryHtml, galleryUseHtml } from "../flow-gallery-ui.js";
|
|
14
|
+
import { galleryDefaults, galleryDiagram, galleryTemplateOf, galleryToolsOf, previewGallery, useGalleryTemplate } from "../flow-gallery.js";
|
|
15
|
+
import { flowInsights } from "../flow-insights.js";
|
|
16
|
+
import { assignFlowCard, commentOnFlowCard, watchFlowCard } from "../flow-people.js";
|
|
17
|
+
import { saveScript } from "../flow-scripts.js";
|
|
18
|
+
import { chooseFlowCard } from "../flow-send.js";
|
|
19
|
+
import { exportFlow, fetchFlowFile, FlowFileError, importFlow, parseFlowFile, planFlowImport } from "../flow-share.js";
|
|
20
|
+
import { addFlowTriggerTo, checkFlowTriggerNow, pressFlowButton, removeFlowTrigger, removeLinearKey, renewFlowHook, saveHooksBase, saveLinearKey, saveLinearSigningSecret, shareFlowButton, stopSharingFlowButton } from "../flow-triggers.js";
|
|
21
|
+
import { FLOW_IMPORT_SCRIPT, flowFallbackHtml, flowImportHtml, flowsListHtml, flowView } from "../flows-ui.js";
|
|
22
|
+
import { FLOW_TEMPLATES, FlowContractError, validateFlowDefinition, withZoneNames } from "../flows.js";
|
|
23
|
+
import { keyStatus } from "../keys.js";
|
|
24
|
+
import { kitPageHtml, kitsGalleryHtml } from "../kits-ui.js";
|
|
25
|
+
import { addKitGithubTrigger, addKitSample, kitOf, setUpKit } from "../kits.js";
|
|
26
|
+
import { connectionsOf } from "../mcp-connect.js";
|
|
27
|
+
import { authorizedProject, projectName, rowVisible } from "../project.js";
|
|
28
|
+
import { recipeAnswersFromForm, recipeDefinitionPreviewHtml, recipeEditorHtml, recipeFromForm, recipeLibraryHtml, recipeRunHtml, recipeScript, workflowPreviewHtml } from "../recipe-ui.js";
|
|
29
|
+
import { createWorkflowPreview, exportRecipe, findRecipe, importRecipe, launchWorkflow, prepareRecipeRun, RecipeError, savedRecipes, saveWorkflowRecipe, starterRecipes, workflowPreview } from "../recipes.js";
|
|
30
|
+
import { isAlive as runnerAlive } from "../runner.js";
|
|
31
|
+
import { createTeammateFrom, labelOf, nameOf, saveSoul, sendTeammateSummaries, setTeammateState, teammateSettings } from "../teammate-admin.js";
|
|
32
|
+
import { addRoutine, removeRoutine, runRoutine } from "../teammate-desk.js";
|
|
33
|
+
import { editMemory, forgetMemory, tellTeammate } from "../teammate-memory.js";
|
|
34
|
+
import { grantTool, revokeTool, rulesFromForm, setToolRules } from "../teammate-tools.js";
|
|
35
|
+
import { sendTeammateWeeklies, undoCall } from "../teammate-week.js";
|
|
36
|
+
import { answerTeammateQuestion } from "../teammate-work.js";
|
|
37
|
+
import { BLANK_SOUL, teammatePageHtml, teammatesListHtml } from "../teammates-ui.js";
|
|
38
|
+
import { escape, redirect, refuse, requestContext, respond, SAFETY, screen, taskHref } from "./shared.js";
|
|
39
|
+
export function createFlowsHandlers(runtime) {
|
|
40
|
+
const { store, clock, sendScreen, chromeFor, consoleProjects, options, visible, flowRooms, identify, liveCeiling, providerHome, toolHome, restricted, evidenceRoot, authenticateApprover, projectOf, unscopedMode, ceiling, authorizeMutation } = runtime;
|
|
41
|
+
async function get(ctx) {
|
|
42
|
+
const { url, who, request, response, now, project, chosenProject, posted, route } = ctx;
|
|
43
|
+
// Flows → New: the gallery, and each template's page.
|
|
44
|
+
if (url.pathname === "/flows/new") {
|
|
45
|
+
const projects = consoleProjects();
|
|
46
|
+
const canUse = who.via === "cookie" && who.role === "approver" && projects.length > 0;
|
|
47
|
+
// The chosen project, else the one being looked at: its tools are what each card says is connected.
|
|
48
|
+
const asked = url.searchParams.get("repo");
|
|
49
|
+
const repo = asked !== null && projects.includes(asked) ? asked : project !== null && projects.includes(project) ? project : null;
|
|
50
|
+
return sendScreen(response, 200, screen("New flow", `<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") }));
|
|
51
|
+
}
|
|
52
|
+
const galleryRead = /^\/flows\/new\/([a-z-]{1,40})$/.exec(url.pathname);
|
|
53
|
+
if (galleryRead !== null) {
|
|
54
|
+
const template = galleryTemplateOf(galleryRead[1]);
|
|
55
|
+
const projects = consoleProjects();
|
|
56
|
+
if (template === null)
|
|
57
|
+
return refuse(response, who, 404, "There's no template by that name.", "/flows/new");
|
|
58
|
+
if (who.via !== "cookie" || who.role !== "approver")
|
|
59
|
+
return refuse(response, who, 403, "Sign in as an approver to create flows.", "/flows/new");
|
|
60
|
+
if (projects.length === 0)
|
|
61
|
+
return refuse(response, who, 409, "Add a project first.", "/projects");
|
|
62
|
+
const asked = url.searchParams.get("repo");
|
|
63
|
+
const repo = asked !== null && projects.includes(asked) ? asked : project !== null && projects.includes(project) ? project : projects[0];
|
|
64
|
+
// Back from a Connect started on this page: how it went.
|
|
65
|
+
const problem = url.searchParams.get("problem"), said = url.searchParams.get("said");
|
|
66
|
+
return sendGalleryUse(response, who, template, projects, repo, null, template.name, problem === null ? null : problem.slice(0, 400), said === null ? null : said.slice(0, 400));
|
|
67
|
+
}
|
|
68
|
+
if (url.pathname === "/flows") {
|
|
69
|
+
const projects = consoleProjects();
|
|
70
|
+
const flows = store.listFlows(projects);
|
|
71
|
+
return sendScreen(response, 200, screen("Flows", `<h1>Flows</h1>${flowsListHtml(store, flows, projects, who.via === "cookie" ? who.session.csrf : "", who.via === "cookie" && who.role === "approver", url.searchParams.get("problem"))}`, { chrome: chromeFor(project, "flows"), functional: { script: FLOW_IMPORT_SCRIPT } }));
|
|
72
|
+
}
|
|
73
|
+
// starter kits — the gallery, and each kit's checklist in a project.
|
|
74
|
+
if (url.pathname === "/kits" || /^\/kits\/[a-z-]{1,40}$/.test(url.pathname)) {
|
|
75
|
+
const projects = consoleProjects();
|
|
76
|
+
const csrf = who.via === "cookie" ? who.session.csrf : "", canSetUp = who.via === "cookie" && who.role === "approver";
|
|
77
|
+
if (url.pathname === "/kits") {
|
|
78
|
+
return sendScreen(response, 200, screen("Starter kits", `<h1>Starter kits</h1>${kitsGalleryHtml(store, projects, projectName, csrf, canSetUp, { problem: url.searchParams.get("problem") })}`, { chrome: chromeFor(project, "flows") }));
|
|
79
|
+
}
|
|
80
|
+
const kit = kitOf(url.pathname.slice("/kits/".length));
|
|
81
|
+
const repo = url.searchParams.get("repo") ?? (projects.length === 1 ? projects[0] : "");
|
|
82
|
+
if (kit === null || !projects.includes(repo))
|
|
83
|
+
return redirect(response, "/kits");
|
|
84
|
+
return sendScreen(response, 200, screen(kit.name, `<h1>${escape(kit.name)}</h1>${kitPageHtml(store, kit, repo, options.configDir ?? null, csrf, canSetUp, { said: url.searchParams.get("said"), problem: url.searchParams.get("problem") })}`, { chrome: chromeFor(repo, "flows") }));
|
|
85
|
+
}
|
|
86
|
+
// v92: AI teammates — the team, and one page per teammate.
|
|
87
|
+
if (url.pathname === "/teammates") {
|
|
88
|
+
const projects = consoleProjects();
|
|
89
|
+
return sendScreen(response, 200, screen("Teammates", `<h1>Teammates</h1>${teammatesListHtml(store, store.teammates(projects), projects, projectName, who.via === "cookie" ? who.session.csrf : "", who.via === "cookie" && who.role === "approver", { said: url.searchParams.get("said"), problem: url.searchParams.get("problem") })}`, { chrome: chromeFor(project, "flows") }));
|
|
90
|
+
}
|
|
91
|
+
const teammateRead = /^\/teammates\/([1-9][0-9]{0,9})(\/soul\.md)?$/.exec(url.pathname);
|
|
92
|
+
if (teammateRead !== null) {
|
|
93
|
+
const mate = store.getTeammate(Number(teammateRead[1]));
|
|
94
|
+
if (mate === null || mate.state === "removed" || !visible(mate.repo) || !store.accountCanAccess(who.name, mate.repo))
|
|
95
|
+
return refuse(response, who, 404, "No such teammate in your projects.", "/teammates");
|
|
96
|
+
if (teammateRead[2] !== undefined) {
|
|
97
|
+
response.writeHead(200, { ...SAFETY, "Content-Type": "text/markdown; charset=utf-8", "Content-Disposition": `attachment; filename="${mate.handle}.md"` });
|
|
98
|
+
return void response.end(mate.soul);
|
|
99
|
+
}
|
|
100
|
+
const approvers = store.listApprovers().map(one => one.name).filter(name => store.accountCanAccess(name, mate.repo));
|
|
101
|
+
return sendScreen(response, 200, screen(labelOf(mate), `<h1>${escape(labelOf(mate))}</h1>${teammatePageHtml(store, mate, who.name, projectName, who.via === "cookie" ? who.session.csrf : "", 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, "flows") }));
|
|
102
|
+
}
|
|
103
|
+
const flowRead = /^\/flows\/([1-9][0-9]{0,9})\/(insights|runs\/([1-9][0-9]{0,9})\/([1-9][0-9]{0,9}))$/.exec(url.pathname);
|
|
104
|
+
if (flowRead !== null) {
|
|
105
|
+
const flow = store.getFlow(Number(flowRead[1]));
|
|
106
|
+
const json = (status, payload) => respond(response, status, "application/json; charset=utf-8", JSON.stringify(payload));
|
|
107
|
+
if (flow === null || flow.state !== "active" || !visible(flow.repo))
|
|
108
|
+
return json(404, { said: "No such flow in your projects." });
|
|
109
|
+
if (flowRead[2] === "insights")
|
|
110
|
+
return json(200, flowInsights(store, flow, clock(), Math.min(90, Math.max(1, Number(url.searchParams.get("days")) || 30))));
|
|
111
|
+
const card = store.getFlowCard(Number(flowRead[3]));
|
|
112
|
+
const run = card === null || card.flow !== flow.id ? null : store.flowStepRun(card.id, Number(flowRead[4]));
|
|
113
|
+
if (run === null)
|
|
114
|
+
return json(404, { said: "No such run." });
|
|
115
|
+
return json(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 ?? "" });
|
|
116
|
+
}
|
|
117
|
+
const flowExport = /^\/flows\/([1-9][0-9]{0,9})\/export$/.exec(url.pathname);
|
|
118
|
+
if (flowExport !== null) {
|
|
119
|
+
// A flow as a file (flow-share.ts): zones, paths, trigger settings and scripts; never secrets, addresses, names or cards.
|
|
120
|
+
const flow = store.getFlow(Number(flowExport[1]));
|
|
121
|
+
if (flow === null || flow.state !== "active" || !visible(flow.repo))
|
|
122
|
+
return refuse(response, who, 404, "No such flow in your projects.", "/flows");
|
|
123
|
+
let exported;
|
|
124
|
+
try {
|
|
125
|
+
exported = exportFlow(store, flow, options.configDir ?? null);
|
|
126
|
+
}
|
|
127
|
+
catch (error) {
|
|
128
|
+
return refuse(response, who, 409, error instanceof Error ? error.message : "This flow can't be exported.", `/flows/${flow.id}`);
|
|
129
|
+
}
|
|
130
|
+
response.setHeader("Content-Disposition", `attachment; filename="${exported.fileName}"`);
|
|
131
|
+
return respond(response, 200, "application/json; charset=utf-8", exported.json);
|
|
132
|
+
}
|
|
133
|
+
const flowLive = /^\/flows\/([1-9][0-9]{0,9})\/live$/.exec(url.pathname);
|
|
134
|
+
if (flowLive !== null) {
|
|
135
|
+
// The live canvas (v88): a nudge the moment the flow changes, and who
|
|
136
|
+
// else has it open. Hints only — the page reads the flow the usual way.
|
|
137
|
+
if (who.via !== "cookie")
|
|
138
|
+
return respond(response, 403, "application/json", JSON.stringify({ error: "session" }));
|
|
139
|
+
const flow = store.getFlow(Number(flowLive[1]));
|
|
140
|
+
if (flow === null || flow.state !== "active" || !visible(flow.repo))
|
|
141
|
+
return respond(response, 404, "application/json", JSON.stringify({ error: "flow" }));
|
|
142
|
+
const card = Number(url.searchParams.get("card"));
|
|
143
|
+
response.writeHead(200, { "content-type": "text/event-stream", "cache-control": "no-store", "x-content-type-options": "nosniff", "x-accel-buffering": "no" });
|
|
144
|
+
const name = who.name, repo = flow.repo;
|
|
145
|
+
flowRooms.join(flow.id, {
|
|
146
|
+
name, card: Number.isSafeInteger(card) && card > 0 ? card : null, editing: url.searchParams.get("editing") === "1", response,
|
|
147
|
+
// Rechecked while open, from the cookie and the account (never a request's context): a sign-out or a narrowed account stops it.
|
|
148
|
+
valid: () => { const again = identify(request, false); return again !== null && again.name === name && rowVisible(liveCeiling(), repo) && store.accountCanAccess(name, repo); },
|
|
149
|
+
});
|
|
150
|
+
request.once("close", () => { if (!response.writableEnded)
|
|
151
|
+
response.end(); });
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
const flowPage = /^\/flows\/([1-9][0-9]{0,9})$/.exec(url.pathname);
|
|
155
|
+
if (flowPage !== null) {
|
|
156
|
+
const flow = store.getFlow(Number(flowPage[1]));
|
|
157
|
+
if (flow === null || flow.state !== "active" || !visible(flow.repo))
|
|
158
|
+
return refuse(response, who, 404, "No such flow in your projects.", "/flows");
|
|
159
|
+
const selected = Number(url.searchParams.get("card")), start = Number(url.searchParams.get("start"));
|
|
160
|
+
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,
|
|
161
|
+
sortReady: keyStatus("openrouter", providerHome).set, toolHome });
|
|
162
|
+
if (url.searchParams.get("format") === "json")
|
|
163
|
+
return respond(response, 200, "application/json; charset=utf-8", JSON.stringify(checkResponse("flowView", view)));
|
|
164
|
+
return sendScreen(response, 200, screen(flow.name, `<p><a href="/flows">Flows</a></p><h1>${escape(flow.name)}</h1>${flowFallbackHtml(view)}`, { chrome: chromeFor(flow.repo, "flows"), workspace: { view } }));
|
|
165
|
+
}
|
|
166
|
+
if (url.pathname === "/recipes" || url.pathname.startsWith("/recipes/")) {
|
|
167
|
+
try {
|
|
168
|
+
const csrf = who.via === "cookie" ? who.session.csrf : "";
|
|
169
|
+
const revision = who.via === "cookie" ? who.session.projectRevision : 0;
|
|
170
|
+
const render = (html) => sendScreen(response, 200, screen("Workflow recipes", html, { chrome: chromeFor(project, "recipes"), functional: { script: recipeScript() } }));
|
|
171
|
+
if (url.pathname === "/recipes") {
|
|
172
|
+
const recent = project === null ? [] : store.handle.prepare("SELECT document,task_id,routine_id FROM workflow_preview WHERE repo=? AND (task_id IS NOT NULL OR routine_id IS NOT NULL) ORDER BY created_at DESC LIMIT 12").all(project).map(row => {
|
|
173
|
+
const d = importRecipe(String(row["document"]));
|
|
174
|
+
const task = row["task_id"] === null ? null : store.getTask(String(row["task_id"]));
|
|
175
|
+
const routine = row["routine_id"] === null ? null : store.getRoutine(Number(row["routine_id"]));
|
|
176
|
+
return { name: d.name, href: task !== null ? taskHref(task.id) : `/routines/${routine?.id}`, state: task?.state ?? (routine?.approvedAt === null ? "Needs approval" : routine?.paused ? "Paused" : "Scheduled") };
|
|
177
|
+
});
|
|
178
|
+
return render(recipeLibraryHtml(starterRecipes(), project === null ? [] : savedRecipes(store, who.name, project), project, csrf, revision, recent));
|
|
179
|
+
}
|
|
180
|
+
if (project === null)
|
|
181
|
+
return refuse(response, who, 400, "Open a project to customize a recipe.", "/projects");
|
|
182
|
+
const token = url.searchParams.get("preview");
|
|
183
|
+
const preview = token === null ? null : workflowPreview(store, who.name, project, token);
|
|
184
|
+
if (token !== null && preview === null)
|
|
185
|
+
return refuse(response, who, 404, "No preview in this project.", "/recipes");
|
|
186
|
+
const sourceRevision = url.searchParams.get("revision");
|
|
187
|
+
if (sourceRevision !== null && !/^[1-9][0-9]{0,8}$/.test(sourceRevision))
|
|
188
|
+
throw new RecipeError("Choose a valid recipe version.");
|
|
189
|
+
let recipe = preview === null ? findRecipe(store, who.name, project, url.searchParams.get("recipe") ?? "", sourceRevision === null ? undefined : Number(sourceRevision))
|
|
190
|
+
: { id: preview.source, revision: 1, repo: project, document: preview.document, digest: preview.digest, author: who.name };
|
|
191
|
+
if (url.pathname === "/recipes/new") {
|
|
192
|
+
const starter = starterRecipes().find(one => one.id === "small-feature");
|
|
193
|
+
recipe = { ...starter, id: "custom", document: { ...starter.document, name: "", description: "", goal: "", outOfScope: null, acceptance: [] } };
|
|
194
|
+
}
|
|
195
|
+
if (url.pathname === "/recipes/start" && recipe?.id === "small-feature") {
|
|
196
|
+
// This starter supplies the process, but the person supplies the
|
|
197
|
+
// feature. Do not let instructional placeholder text become work.
|
|
198
|
+
recipe = { ...recipe, document: { ...recipe.document, goal: "" } };
|
|
199
|
+
}
|
|
200
|
+
if (url.pathname === "/recipes/from-task") {
|
|
201
|
+
const id = url.searchParams.get("task") ?? "";
|
|
202
|
+
const ref = store.lookupRef(id), task = store.getTask(id), scope = store.getScope(id);
|
|
203
|
+
if (ref?.repo !== project || task === null || scope === null)
|
|
204
|
+
throw new RecipeError("No scoped task in this project.", 404);
|
|
205
|
+
recipe = { id: "task-copy", revision: 1, repo: project, digest: "", author: who.name, document: {
|
|
206
|
+
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,
|
|
207
|
+
planning: "auto", deliverable: ref.deliverable === "report" ? "report" : "branch", schedule: null, costCeilingUsd: null,
|
|
208
|
+
} };
|
|
209
|
+
}
|
|
210
|
+
if (url.pathname === "/recipes/run") {
|
|
211
|
+
if (recipe === null || recipe.repo === null)
|
|
212
|
+
throw new RecipeError("Save a project recipe before using it here.", 404);
|
|
213
|
+
return render(recipeRunHtml(recipe, project, csrf, revision, null, undefined, url.searchParams.get("saved") === "1"));
|
|
214
|
+
}
|
|
215
|
+
if (url.pathname === "/recipes/preview" && preview !== null) {
|
|
216
|
+
if (preview.document.version === 2 || url.searchParams.get("purpose") === "recipe")
|
|
217
|
+
return render(recipeDefinitionPreviewHtml(preview, csrf, revision));
|
|
218
|
+
const agents = resolveRoutineAuthority(store, project, preview.document.acceptance, now);
|
|
219
|
+
const workers = store.listRunners().filter(one => one.retiredAt === null && one.repos.includes(project) && runnerAlive(one, now));
|
|
220
|
+
const mode = store.activeMode(project, now);
|
|
221
|
+
const readiness = [
|
|
222
|
+
{ title: "Project", detail: projectName(project) },
|
|
223
|
+
{ 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" } : {}) },
|
|
224
|
+
{ 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." },
|
|
225
|
+
{ 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" } : {}) },
|
|
226
|
+
];
|
|
227
|
+
return render(workflowPreviewHtml(preview, csrf, revision, now, readiness, mode?.signedBy === who.name));
|
|
228
|
+
}
|
|
229
|
+
if (recipe === null)
|
|
230
|
+
return refuse(response, who, 404, "No recipe in this project.", "/recipes");
|
|
231
|
+
if (url.pathname === "/recipes/export") {
|
|
232
|
+
response.setHeader("Content-Disposition", 'attachment; filename="standing-orders-recipe.json"');
|
|
233
|
+
return respond(response, 200, "application/json; charset=utf-8", exportRecipe(recipe.document));
|
|
234
|
+
}
|
|
235
|
+
if (["/recipes/start", "/recipes/new", "/recipes/edit", "/recipes/from-task"].includes(url.pathname))
|
|
236
|
+
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"));
|
|
237
|
+
return refuse(response, who, 404, "No recipe screen here.", "/recipes");
|
|
238
|
+
}
|
|
239
|
+
catch (error) {
|
|
240
|
+
if (error instanceof RecipeError)
|
|
241
|
+
return refuse(response, who, error.status, error.message, "/recipes");
|
|
242
|
+
throw error;
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
return refuse(response, who, 404, "There's no page at this address.", "/chat");
|
|
246
|
+
}
|
|
247
|
+
async function post(ctx) {
|
|
248
|
+
const { url, who, request, response, now, project, chosenProject, posted, route } = ctx;
|
|
249
|
+
// setting a starter kit up, trying its sample card, and bringing GitHub issues in.
|
|
250
|
+
const kitPost = /^\/kits\/([a-z-]{1,40})\/(setup|sample|github)$/.exec(url.pathname);
|
|
251
|
+
if (kitPost !== null) {
|
|
252
|
+
const body = readForm(posted, CONSOLE_FORMS.kit);
|
|
253
|
+
const now = clock();
|
|
254
|
+
if (who.via !== "cookie" || who.role !== "approver")
|
|
255
|
+
return refuse(response, who, 403, "Sign in as an approver to set kits up.", "/kits");
|
|
256
|
+
const kit = kitOf(kitPost[1]);
|
|
257
|
+
const projects = consoleProjects();
|
|
258
|
+
const repo = body.get("repo") ?? "";
|
|
259
|
+
if (kit === null || !projects.includes(repo) || !store.accountCanAccess(who.name, repo))
|
|
260
|
+
return redirect(response, `/kits?problem=${encodeURIComponent("Choose one of your projects.")}`);
|
|
261
|
+
const page = (key, words) => redirect(response, `/kits/${kit.id}?repo=${encodeURIComponent(repo)}&${key}=${encodeURIComponent(words)}`);
|
|
262
|
+
if (kitPost[2] === "setup") {
|
|
263
|
+
const made = await setUpKit(store, kit, repo, who.name, now, options.configDir ?? null, { toolHome });
|
|
264
|
+
return page(made.ok ? "said" : "problem", made.said);
|
|
265
|
+
}
|
|
266
|
+
if (kitPost[2] === "github") {
|
|
267
|
+
const made = addKitGithubTrigger(store, kit, repo, who.name, now, options.configDir ?? null);
|
|
268
|
+
return page(made.ok ? "said" : "problem", made.said);
|
|
269
|
+
}
|
|
270
|
+
const tried = addKitSample(store, kit, repo, who.name, now);
|
|
271
|
+
if (!tried.ok)
|
|
272
|
+
return page("problem", tried.said);
|
|
273
|
+
// The teammate picks it up on the worker's next pass (a few seconds); its flow opens on the card.
|
|
274
|
+
try {
|
|
275
|
+
advanceFlows(store, repo, now, { evidenceRoot });
|
|
276
|
+
}
|
|
277
|
+
catch { /* the worker's next pass moves it */ }
|
|
278
|
+
return redirect(response, tried.href);
|
|
279
|
+
}
|
|
280
|
+
// v92: looking after teammates, and answering their questions.
|
|
281
|
+
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);
|
|
282
|
+
const questionPost = /^\/teammates\/questions\/([1-9][0-9]{0,9})\/answer$/.exec(url.pathname);
|
|
283
|
+
if (teammatePost !== null || questionPost !== null) {
|
|
284
|
+
const body = readForm(posted, CONSOLE_FORMS.teammates);
|
|
285
|
+
const now = clock();
|
|
286
|
+
const wantsJson = (request.headers.accept ?? "").includes("application/json");
|
|
287
|
+
if (who.via !== "cookie")
|
|
288
|
+
return refuse(response, who, 403, "Sign in to do that.", "/teammates");
|
|
289
|
+
if (questionPost !== null) {
|
|
290
|
+
const question = store.teammateQuestion(Number(questionPost[1]));
|
|
291
|
+
const card = question === null ? null : store.getFlowCard(question.card);
|
|
292
|
+
const flow = card === null ? null : store.getFlow(card.flow);
|
|
293
|
+
const done = question === null || flow === null || !visible(flow.repo) ? { ok: false, said: "No such question." }
|
|
294
|
+
: answerTeammateQuestion(store, question.id, { choice: body.get("choice"), text: body.get("text"), by: who.name, via: "web" }, now);
|
|
295
|
+
if (wantsJson)
|
|
296
|
+
return respond(response, done.ok ? 200 : 409, "application/json; charset=utf-8", JSON.stringify(done));
|
|
297
|
+
const back = question === null ? "/teammates" : `/teammates/${question.teammate}`;
|
|
298
|
+
return redirect(response, `${back}?${done.ok ? "said" : "problem"}=${encodeURIComponent(done.said)}`);
|
|
299
|
+
}
|
|
300
|
+
if (who.role !== "approver")
|
|
301
|
+
return refuse(response, who, 403, "Sign in as an approver to look after teammates.", "/teammates");
|
|
302
|
+
if (teammatePost[2] === "new") {
|
|
303
|
+
const projects = consoleProjects();
|
|
304
|
+
const repo = body.get("repo") ?? "";
|
|
305
|
+
if (!projects.includes(repo))
|
|
306
|
+
return redirect(response, `/teammates?problem=${encodeURIComponent("Choose one of your projects.")}`);
|
|
307
|
+
const template = body.get("template") ?? "";
|
|
308
|
+
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);
|
|
309
|
+
return made.ok ? redirect(response, `/teammates/${made.id}?said=${encodeURIComponent(made.said)}`) : redirect(response, `/teammates?problem=${encodeURIComponent(made.said)}`);
|
|
310
|
+
}
|
|
311
|
+
const mate = store.getTeammate(Number(teammatePost[1]));
|
|
312
|
+
if (mate === null || mate.state === "removed" || !visible(mate.repo) || !store.accountCanAccess(who.name, mate.repo))
|
|
313
|
+
return refuse(response, who, 404, "No such teammate in your projects.", "/teammates");
|
|
314
|
+
const back = `/teammates/${mate.id}`;
|
|
315
|
+
const action = teammatePost[2];
|
|
316
|
+
if (action === "soul") {
|
|
317
|
+
const soul = body.get("soul") ?? "";
|
|
318
|
+
const saved = saveSoul(store, mate, soul, who.name, now);
|
|
319
|
+
if (!saved.ok) {
|
|
320
|
+
const approvers = store.listApprovers().map(one => one.name).filter(name => store.accountCanAccess(name, mate.repo));
|
|
321
|
+
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") }));
|
|
322
|
+
}
|
|
323
|
+
return redirect(response, `${back}?said=${encodeURIComponent(saved.said)}`);
|
|
324
|
+
}
|
|
325
|
+
if (action === "week") {
|
|
326
|
+
// v97: undo one of its tool calls, or send the week's report now.
|
|
327
|
+
const done = body.get("op") === "undo" ? await undoCall(store, mate, Number(body.get("id")), who.name, { toolHome }, now)
|
|
328
|
+
: sendTeammateWeeklies(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." };
|
|
329
|
+
if (wantsJson)
|
|
330
|
+
return respond(response, done.ok ? 200 : 409, "application/json; charset=utf-8", JSON.stringify(done));
|
|
331
|
+
return redirect(response, `${back}?${done.ok ? "said" : "problem"}=${encodeURIComponent(done.said)}#week`);
|
|
332
|
+
}
|
|
333
|
+
if (action === "routines") {
|
|
334
|
+
// v96: its routines: add one, try one now, or remove one.
|
|
335
|
+
const op = body.get("op"), routine = Number(body.get("id"));
|
|
336
|
+
const hooks = options.configDir ?? null;
|
|
337
|
+
const changed = op === "add" ? addRoutine(store, mate, body.get("schedule") ?? "", body.get("text") ?? "", who.name, now, hooks)
|
|
338
|
+
: op === "run" ? runRoutine(store, mate, routine, who.name, now)
|
|
339
|
+
: removeRoutine(store, mate, routine, now, hooks);
|
|
340
|
+
if (changed.ok && op === "run") {
|
|
341
|
+
try {
|
|
342
|
+
advanceFlows(store, mate.repo, now, { evidenceRoot });
|
|
343
|
+
}
|
|
344
|
+
catch { /* the worker's next pass moves it */ }
|
|
345
|
+
}
|
|
346
|
+
return redirect(response, `${back}?${changed.ok ? "said" : "problem"}=${encodeURIComponent(changed.said)}#desk`);
|
|
347
|
+
}
|
|
348
|
+
if (action === "memory") {
|
|
349
|
+
// v95: edit or forget one thing it remembers.
|
|
350
|
+
const memory = Number(body.get("id"));
|
|
351
|
+
const changed = body.get("op") === "forget" ? forgetMemory(store, mate, memory, who.name, now) : editMemory(store, mate, memory, body.get("text") ?? "", who.name, now);
|
|
352
|
+
return redirect(response, `${back}?${changed.ok ? "said" : "problem"}=${encodeURIComponent(changed.said)}#memory`);
|
|
353
|
+
}
|
|
354
|
+
if (action === "tools") {
|
|
355
|
+
// v94: which project tools it may use, and its rule for each action.
|
|
356
|
+
const tool = body.get("tool") ?? "", op = body.get("op");
|
|
357
|
+
const grant = store.teammateGrant(mate.id, tool);
|
|
358
|
+
const changed = op === "grant" ? await grantTool(store, mate, tool, who.name, now, { toolHome })
|
|
359
|
+
: op === "revoke" ? revokeTool(store, mate, tool, who.name, now)
|
|
360
|
+
: 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);
|
|
361
|
+
return redirect(response, `${back}?${changed.ok ? "said" : "problem"}=${encodeURIComponent(changed.said)}#tools`);
|
|
362
|
+
}
|
|
363
|
+
const done = action === "state" ? setTeammateState(store, mate, body.get("state") === "removed" ? "removed" : body.get("state") === "paused" ? "paused" : "active", who.name, now)
|
|
364
|
+
: action === "note" ? tellTeammate(store, mate, body.get("note") ?? "", who.name, now)
|
|
365
|
+
: action === "settings" ? teammateSettings(store, mate, { model: body.get("model") ?? "default", dailyTurns: Number(body.get("dailyTurns")), manager: body.get("manager") ?? mate.manager }, who.name, now)
|
|
366
|
+
: sendTeammateSummaries(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." };
|
|
367
|
+
if (action === "state" && body.get("state") === "removed" && done.ok)
|
|
368
|
+
return redirect(response, `/teammates?said=${encodeURIComponent(done.said)}`);
|
|
369
|
+
return redirect(response, `${back}?${done.ok ? "said" : "problem"}=${encodeURIComponent(done.said)}${action === "note" ? "#memory" : ""}`);
|
|
370
|
+
}
|
|
371
|
+
// Flows → New → a template: preview these answers, or create exactly what was previewed.
|
|
372
|
+
const galleryPost = /^\/flows\/new\/([a-z-]{1,40})$/.exec(url.pathname);
|
|
373
|
+
if (galleryPost !== null) {
|
|
374
|
+
const body = readForm(posted, CONSOLE_FORMS.galleryUse);
|
|
375
|
+
const template = galleryTemplateOf(galleryPost[1]);
|
|
376
|
+
const projects = consoleProjects();
|
|
377
|
+
if (template === null)
|
|
378
|
+
return refuse(response, who, 404, "There's no template by that name.", "/flows/new");
|
|
379
|
+
if (who.via !== "cookie" || who.role !== "approver" || store.isDemo())
|
|
380
|
+
return refuse(response, who, 403, "Sign in as an approver to create flows.", "/flows/new");
|
|
381
|
+
const repo = body.get("repo") ?? "";
|
|
382
|
+
if (!projects.includes(repo))
|
|
383
|
+
return refuse(response, who, 404, "Choose one of your projects.", "/flows/new");
|
|
384
|
+
const answers = { ...Object.fromEntries(template.asks.map(ask => [ask.key, body.get(ask.key) ?? ""])), ...(body.get("send-result") === "yes" ? { "send-result": "yes" } : {}) };
|
|
385
|
+
const name = (body.get("name") ?? "").replace(/[\u0000-\u001f\u007f]/g, "").trim().slice(0, 80) || template.name;
|
|
386
|
+
if (body.get("intent") !== "create")
|
|
387
|
+
return sendGalleryUse(response, who, template, projects, repo, answers, name, null);
|
|
388
|
+
// What's made is what was previewed: changed answers are previewed again first.
|
|
389
|
+
let digest = null;
|
|
390
|
+
try {
|
|
391
|
+
digest = previewGallery(store, template, repo, answers, who.name, clock()).digest;
|
|
392
|
+
}
|
|
393
|
+
catch {
|
|
394
|
+
digest = null;
|
|
395
|
+
}
|
|
396
|
+
if (digest === null || digest !== body.get("previewed"))
|
|
397
|
+
return sendGalleryUse(response, who, template, projects, repo, answers, name, digest === null ? null : "Your answers changed. Check the preview, then create the flow.");
|
|
398
|
+
const used = useGalleryTemplate(store, template, repo, answers, { name, by: who.name, now: clock(), dir: options.configDir ?? null });
|
|
399
|
+
if (!used.ok)
|
|
400
|
+
return sendGalleryUse(response, who, template, projects, repo, answers, name, used.said);
|
|
401
|
+
return redirect(response, `/flows/${used.flow}`);
|
|
402
|
+
}
|
|
403
|
+
const flowPost = /^\/flows\/([1-9][0-9]{0,9})\/(save|cards|archive|triggers|linear-key|hooks-address|scripts|secrets)$/.exec(url.pathname) ?? /^\/flows\/([1-9][0-9]{0,9})\/cards\/([1-9][0-9]{0,9})\/(move|decide|choose|cancel|comment|assign|watch)$/.exec(url.pathname);
|
|
404
|
+
const triggerPost = /^\/flows\/([1-9][0-9]{0,9})\/triggers\/([1-9][0-9]{0,9})\/(pause|resume|remove|check|press|renew|secret|share|unshare)$/.exec(url.pathname);
|
|
405
|
+
if (url.pathname === "/flows/new" || url.pathname === "/flows/example" || url.pathname === "/flows/import" || flowPost !== null || triggerPost !== null) {
|
|
406
|
+
const body = readForm(posted, CONSOLE_FORMS.flows);
|
|
407
|
+
const now = clock();
|
|
408
|
+
const answer = (status, payload) => respond(response, status, "application/json; charset=utf-8", JSON.stringify(payload));
|
|
409
|
+
const projects = consoleProjects();
|
|
410
|
+
if (who.via !== "cookie" || who.role !== "approver")
|
|
411
|
+
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." });
|
|
412
|
+
if (url.pathname === "/flows/example") {
|
|
413
|
+
// A first look (v88): the Email replies template with one sample question, which Claude drafts a reply to straight away.
|
|
414
|
+
const repo = body.get("repo") ?? "";
|
|
415
|
+
if (!projects.includes(repo))
|
|
416
|
+
return redirect(response, `/flows?problem=${encodeURIComponent("Choose one of your projects.")}`);
|
|
417
|
+
const template = FLOW_TEMPLATES.find(one => one.id === "email-replies");
|
|
418
|
+
const id = store.createFlow({ repo, name: "Customer replies (example)", definitionJson: JSON.stringify(template.definition), by: who.name }, now);
|
|
419
|
+
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);
|
|
420
|
+
return redirect(response, `/flows/${id}`);
|
|
421
|
+
}
|
|
422
|
+
if (url.pathname === "/flows/import") {
|
|
423
|
+
// A flow file, chosen or fetched from a gist: previewed in plain words, then made with its triggers off and its scripts held.
|
|
424
|
+
const repo = body.get("repo") ?? "";
|
|
425
|
+
const back = (problem) => redirect(response, `/flows?problem=${encodeURIComponent(problem)}`);
|
|
426
|
+
if (!projects.includes(repo))
|
|
427
|
+
return back("Choose one of your projects.");
|
|
428
|
+
let document = body.get("document") ?? "";
|
|
429
|
+
const address = (body.get("url") ?? "").trim();
|
|
430
|
+
let file;
|
|
431
|
+
try {
|
|
432
|
+
if (document.trim() === "" && address !== "")
|
|
433
|
+
document = await fetchFlowFile(address, options.flowFetch);
|
|
434
|
+
if (document.trim() === "")
|
|
435
|
+
return back("Choose a flow file or give its address.");
|
|
436
|
+
file = parseFlowFile(document);
|
|
437
|
+
}
|
|
438
|
+
catch (error) {
|
|
439
|
+
if (error instanceof FlowFileError)
|
|
440
|
+
return back(error.message);
|
|
441
|
+
throw error;
|
|
442
|
+
}
|
|
443
|
+
const given = Object.fromEntries(file.parameters.flatMap(one => body.has(`param.${one.id}`) ? [[one.id, body.get(`param.${one.id}`) ?? ""]] : []));
|
|
444
|
+
// What was previewed: the file and every value it resolved to (a default the first preview filled in included).
|
|
445
|
+
const previewedOf = (plan) => createHash("sha256").update(`${document}\0${JSON.stringify(plan?.values ?? given)}`).digest("hex").slice(0, 32);
|
|
446
|
+
const page = (plan, problem, status) => sendScreen(response, status, screen(`Import ${file.name}`, `<p><a href="/flows">Flows</a></p><h1>Import ${escape(file.name)}</h1>${flowImportHtml({ plan, file, document, repo, csrf: who.session.csrf, values: given, previewed: previewedOf(plan), problem })}`, { chrome: chromeFor(repo, "flows") }));
|
|
447
|
+
let plan;
|
|
448
|
+
try {
|
|
449
|
+
plan = planFlowImport(store, repo, file, given, who.name);
|
|
450
|
+
}
|
|
451
|
+
catch (error) {
|
|
452
|
+
if (error instanceof FlowFileError)
|
|
453
|
+
return page(null, error.message, 400);
|
|
454
|
+
throw error;
|
|
455
|
+
}
|
|
456
|
+
if (body.get("confirm") === "yes") {
|
|
457
|
+
if (body.get("previewed") !== previewedOf(plan))
|
|
458
|
+
return page(plan, "What you filled in changed. Check the preview again, then import.", 409);
|
|
459
|
+
try {
|
|
460
|
+
return redirect(response, `/flows/${importFlow(store, plan, who.name, now, options.configDir ?? null).id}`);
|
|
461
|
+
}
|
|
462
|
+
catch (error) {
|
|
463
|
+
return page(plan, error instanceof Error ? error.message : "That flow couldn't be imported.", 400);
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
return page(plan, null, 200);
|
|
467
|
+
}
|
|
468
|
+
if (url.pathname === "/flows/new") {
|
|
469
|
+
const repo = body.get("repo") ?? "";
|
|
470
|
+
if (!projects.includes(repo))
|
|
471
|
+
return redirect(response, `/flows?problem=${encodeURIComponent("Choose one of your projects.")}`);
|
|
472
|
+
const template = FLOW_TEMPLATES.find(one => one.id === body.get("template")) ?? FLOW_TEMPLATES[0];
|
|
473
|
+
const name = (body.get("name") ?? "").replace(/[\u0000-\u001f\u007f]/g, "").trim().slice(0, 80) || template.label;
|
|
474
|
+
const made = store.createFlow({ repo, name, definitionJson: JSON.stringify(template.definition), by: who.name }, now);
|
|
475
|
+
// A template that starts from something (Issues to PRs: labelled issues) comes with its trigger.
|
|
476
|
+
const trigger = template.trigger === undefined ? null : addFlowTriggerTo(store, store.getFlow(made), template.trigger, who.name, now, options.configDir ?? null);
|
|
477
|
+
if (trigger !== null && !trigger.ok)
|
|
478
|
+
return redirect(response, `/flows?problem=${encodeURIComponent(`${name} was made, but its trigger wasn't: ${trigger.message} Add one on its Triggers panel.`)}`);
|
|
479
|
+
return redirect(response, `/flows/${made}`);
|
|
480
|
+
}
|
|
481
|
+
const flow = store.getFlow(Number((flowPost ?? triggerPost)[1]));
|
|
482
|
+
if (flow === null || flow.state !== "active" || !visible(flow.repo))
|
|
483
|
+
return answer(404, { ok: false, said: "No such flow in your projects." });
|
|
484
|
+
const definition = flowDefinitionOf(flow);
|
|
485
|
+
const dir = options.configDir ?? null;
|
|
486
|
+
const viewNow = () => flowView(store, store.getFlow(flow.id) ?? flow, { name: who.name, approver: true }, null, { dir, repos: projects, sortReady: keyStatus("openrouter", providerHome).set, toolHome });
|
|
487
|
+
const settle = (said, extra = {}) => {
|
|
488
|
+
// Move what can move right away (a message, a decision's next zone); workers file and run the tasks.
|
|
489
|
+
try {
|
|
490
|
+
advanceFlows(store, flow.repo, now, { evidenceRoot });
|
|
491
|
+
}
|
|
492
|
+
catch { /* the next worker pass retries */ }
|
|
493
|
+
return answer(200, { ok: true, said, view: viewNow(), ...extra });
|
|
494
|
+
};
|
|
495
|
+
if (triggerPost !== null) {
|
|
496
|
+
const trigger = store.getFlowTrigger(Number(triggerPost[2]));
|
|
497
|
+
if (trigger === null || trigger.flow !== flow.id || trigger.state === "removed")
|
|
498
|
+
return answer(404, { ok: false, said: "That trigger isn't on this flow." });
|
|
499
|
+
const verb = triggerPost[3];
|
|
500
|
+
if (verb === "pause" || verb === "resume") {
|
|
501
|
+
store.updateFlowTrigger(trigger.id, { state: verb === "pause" ? "paused" : "active" }, now);
|
|
502
|
+
return settle(verb === "pause" ? "Trigger paused." : "Trigger on again.");
|
|
503
|
+
}
|
|
504
|
+
if (verb === "remove") {
|
|
505
|
+
removeFlowTrigger(store, trigger, now, dir);
|
|
506
|
+
return settle("Trigger removed.");
|
|
507
|
+
}
|
|
508
|
+
if (verb === "share") {
|
|
509
|
+
const shared = shareFlowButton(store, trigger, now, dir);
|
|
510
|
+
return shared.ok ? settle(shared.said, { reveal: shared.reveal }) : answer(409, { ok: false, said: shared.message });
|
|
511
|
+
}
|
|
512
|
+
if (verb === "unshare") {
|
|
513
|
+
stopSharingFlowButton(store, trigger, now);
|
|
514
|
+
return settle("The form link no longer works.");
|
|
515
|
+
}
|
|
516
|
+
if (verb === "press") {
|
|
517
|
+
let answers;
|
|
518
|
+
try {
|
|
519
|
+
answers = JSON.parse(body.get("answers") ?? "[]");
|
|
520
|
+
}
|
|
521
|
+
catch {
|
|
522
|
+
answers = [];
|
|
523
|
+
}
|
|
524
|
+
const pressed = pressFlowButton(store, trigger, Array.isArray(answers) ? answers : [], who.name, now);
|
|
525
|
+
return pressed.ok ? settle(pressed.said) : answer(400, { ok: false, said: pressed.message });
|
|
526
|
+
}
|
|
527
|
+
if (verb === "check") {
|
|
528
|
+
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 }),
|
|
529
|
+
// A schedule's script, run now (v90): in a clean folder beside the database, inside the agents' fence.
|
|
530
|
+
shell: options.flowTriggerIo?.shell ?? execRun, scratch: options.flowTriggerIo?.scratch ?? join(dir ?? tmpdir(), "flow-scratch") });
|
|
531
|
+
try {
|
|
532
|
+
advanceFlows(store, flow.repo, now, { evidenceRoot });
|
|
533
|
+
}
|
|
534
|
+
catch { /* the next worker pass retries */ }
|
|
535
|
+
return answer(checked.ok ? 200 : 409, { ok: checked.ok, said: checked.said, view: viewNow() });
|
|
536
|
+
}
|
|
537
|
+
if (dir === null)
|
|
538
|
+
return answer(409, { ok: false, said: "Webhook secrets are kept on the console's computer." });
|
|
539
|
+
if (verb === "renew") {
|
|
540
|
+
const renewed = renewFlowHook(store, trigger, now, dir);
|
|
541
|
+
return renewed.ok ? settle(renewed.said, { reveal: renewed.reveal }) : answer(409, { ok: false, said: renewed.message });
|
|
542
|
+
}
|
|
543
|
+
// Linear's signing secret, pasted on this secure panel behind the password — never in chat.
|
|
544
|
+
if (!authenticateApprover(who, body.get("password") ?? "").ok)
|
|
545
|
+
return answer(403, { ok: false, said: "Enter your Toolroll password to save a secret." });
|
|
546
|
+
const saved = saveLinearSigningSecret(trigger, body.get("secret") ?? "", dir);
|
|
547
|
+
return saved.ok ? settle("Signing secret saved. Linear's deliveries can be proved now.") : answer(400, { ok: false, said: saved.message });
|
|
548
|
+
}
|
|
549
|
+
const action = flowPost[2];
|
|
550
|
+
if (action === "triggers") {
|
|
551
|
+
let raw;
|
|
552
|
+
try {
|
|
553
|
+
raw = JSON.parse(body.get("trigger") ?? "null");
|
|
554
|
+
}
|
|
555
|
+
catch {
|
|
556
|
+
return answer(400, { ok: false, said: "That trigger couldn't be read." });
|
|
557
|
+
}
|
|
558
|
+
const made = addFlowTriggerTo(store, flow, raw, who.name, now, dir);
|
|
559
|
+
return made.ok ? settle(made.said, made.reveal === null ? {} : { reveal: made.reveal }) : answer(400, { ok: false, said: made.message });
|
|
560
|
+
}
|
|
561
|
+
if (action === "scripts") {
|
|
562
|
+
// The project's script library, reached from any of its flows.
|
|
563
|
+
if (body.get("approve") === "yes")
|
|
564
|
+
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." });
|
|
565
|
+
if (body.get("remove") === "yes")
|
|
566
|
+
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." });
|
|
567
|
+
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);
|
|
568
|
+
return saved.ok ? settle(saved.said) : answer(400, { ok: false, said: saved.message });
|
|
569
|
+
}
|
|
570
|
+
if (action === "linear-key" || action === "hooks-address") {
|
|
571
|
+
// Installation settings, set from the flow they are needed on.
|
|
572
|
+
if (dir === null)
|
|
573
|
+
return answer(409, { ok: false, said: "These settings are kept on the console's computer." });
|
|
574
|
+
if (action === "hooks-address") {
|
|
575
|
+
const saved = saveHooksBase(dir, body.get("address") ?? "");
|
|
576
|
+
return saved.ok ? settle(saved.base === null ? "Public address cleared." : "Public address saved.") : answer(400, { ok: false, said: saved.message });
|
|
577
|
+
}
|
|
578
|
+
if (body.get("remove") === "yes") {
|
|
579
|
+
removeLinearKey(dir);
|
|
580
|
+
return settle("Linear key removed.");
|
|
581
|
+
}
|
|
582
|
+
if (!authenticateApprover(who, body.get("password") ?? "").ok)
|
|
583
|
+
return answer(403, { ok: false, said: "Enter your Toolroll password to save a key." });
|
|
584
|
+
const saved = saveLinearKey(dir, body.get("key") ?? "");
|
|
585
|
+
return saved.ok ? settle("Linear key saved on this computer.") : answer(400, { ok: false, said: saved.message });
|
|
586
|
+
}
|
|
587
|
+
if (action === "archive") {
|
|
588
|
+
store.archiveFlow(flow.id, who.name, now);
|
|
589
|
+
return redirect(response, "/flows");
|
|
590
|
+
}
|
|
591
|
+
if (action === "save") {
|
|
592
|
+
let saved, drawn;
|
|
593
|
+
try {
|
|
594
|
+
drawn = JSON.parse(body.get("definition") ?? "null");
|
|
595
|
+
saved = validateFlowDefinition(drawn);
|
|
596
|
+
// A {{stage.…}} this save adds must be one its zone hands on; ones the saved flow had stay.
|
|
597
|
+
const references = stageReferenceProblems(saved, flowDefinitionOf(flow));
|
|
598
|
+
if (references.length > 0)
|
|
599
|
+
throw new FlowContractError(references.map(one => one.line));
|
|
600
|
+
}
|
|
601
|
+
catch (error) {
|
|
602
|
+
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." });
|
|
603
|
+
}
|
|
604
|
+
const name = (body.get("name") ?? flow.name).replace(/[\u0000-\u001f\u007f]/g, "").trim().slice(0, 80) || flow.name;
|
|
605
|
+
// The owner (v86) is whom "the owner decides" zones ask: someone who can approve on this project.
|
|
606
|
+
const owner = (body.get("owner") ?? "").trim();
|
|
607
|
+
if (owner !== "" && owner !== flow.owner && !(store.listApprovers().some(one => one.name === owner) && store.accountCanAccess(owner, flow.repo)))
|
|
608
|
+
return answer(400, { ok: false, said: `${owner} can't approve on this project, so they can't own this flow.` });
|
|
609
|
+
// A build zone in another project files work there as the flow's owner: only in one they may file in.
|
|
610
|
+
const elsewhere = crossProjectProblem(store, saved, { repo: flow.repo, owner: owner || flow.owner }, who.name);
|
|
611
|
+
if (elsewhere !== null)
|
|
612
|
+
return answer(400, { ok: false, said: elsewhere });
|
|
613
|
+
if (!store.saveFlow(flow.id, { name, definitionJson: JSON.stringify(saved), sawRevision: Number(body.get("revision")), by: who.name }, now))
|
|
614
|
+
return answer(409, { ok: false, said: "Someone else changed this flow. Reload to see their changes, then make yours again." });
|
|
615
|
+
if (owner !== "" && owner !== flow.owner)
|
|
616
|
+
store.setFlowOwner(flow.id, owner, now);
|
|
617
|
+
return settle("Saved.");
|
|
618
|
+
}
|
|
619
|
+
if (action === "secrets") {
|
|
620
|
+
// A web request's secret: written here, used only in its headers, never shown back.
|
|
621
|
+
if (dir === null)
|
|
622
|
+
return answer(409, { ok: false, said: "Secrets are kept on the console's computer." });
|
|
623
|
+
const saved = setFlowSecret(dir, flow.repo, (body.get("name") ?? "").trim(), body.get("value") ?? "");
|
|
624
|
+
return saved.ok ? settle(saved.said) : answer(400, { ok: false, said: saved.message });
|
|
625
|
+
}
|
|
626
|
+
if (definition === null)
|
|
627
|
+
return answer(409, { ok: false, said: "This flow's drawing can't be read. Save it again from the editor." });
|
|
628
|
+
if (action === "cards") {
|
|
629
|
+
const added = addCardToFlow(store, flow, { title: body.get("title"), description: body.get("description"), stage: body.get("stage") }, who.name, now);
|
|
630
|
+
return added.ok ? settle("Card added.") : answer(400, { ok: false, said: added.message });
|
|
631
|
+
}
|
|
632
|
+
const target = store.getFlowCard(Number(flowPost[2]));
|
|
633
|
+
if (target === null || target.flow !== flow.id)
|
|
634
|
+
return answer(404, { ok: false, said: "That card isn't in this flow." });
|
|
635
|
+
const verb = flowPost[3];
|
|
636
|
+
if (verb === "move") {
|
|
637
|
+
if (!definition.stages.some(one => one.id === body.get("stage")))
|
|
638
|
+
return answer(400, { ok: false, said: "Choose a zone in this flow." });
|
|
639
|
+
const moved = moveCardInFlow(store, target, body.get("stage") ?? "", who.name, now);
|
|
640
|
+
return !moved.ok ? answer(409, { ok: false, said: moved.message }) : moved.said === "Already there." ? answer(200, { ok: true, said: moved.said }) : settle(moved.said);
|
|
641
|
+
}
|
|
642
|
+
if (verb === "decide") {
|
|
643
|
+
const decision = body.get("decision") === "approve" ? "approve" : body.get("decision") === "send-back" ? "send-back" : null;
|
|
644
|
+
if (decision === null)
|
|
645
|
+
return answer(400, { ok: false, said: "Approve it or send it back." });
|
|
646
|
+
const note = (body.get("note") ?? "").trim() || null;
|
|
647
|
+
if (note !== null && note.length > LIMITS.note)
|
|
648
|
+
return answer(400, { ok: false, said: `Keep the note to ${LIMITS.note.toLocaleString("en-US")} characters; this is ${note.length.toLocaleString("en-US")}.` });
|
|
649
|
+
const decided = decideFlowCard(store, { card: target.id, decision, note, actor: who.name, repos: projects, evidenceRoot, draft: body.get("draft"), where: "the console" }, now);
|
|
650
|
+
return decided.ok ? settle(decided.said) : answer(409, { ok: false, said: decided.message });
|
|
651
|
+
}
|
|
652
|
+
if (verb === "choose") {
|
|
653
|
+
// "Person chooses": one of the zone's options (by its place and words), or a reply that becomes the note.
|
|
654
|
+
const choice = /^[0-9]$/.test(body.get("choice") ?? "") ? Number(body.get("choice")) : null;
|
|
655
|
+
const chosen = chooseFlowCard(store, { card: target.id, ...(/^[1-9][0-9]{0,9}$/.test(body.get("entry") ?? "") ? { entry: Number(body.get("entry")) } : {}), choice,
|
|
656
|
+
...(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);
|
|
657
|
+
return chosen.ok ? settle(chosen.said) : answer(409, { ok: false, said: chosen.message });
|
|
658
|
+
}
|
|
659
|
+
if (verb === "comment") {
|
|
660
|
+
const commented = commentOnFlowCard(store, target, who.name, body.get("body"), now);
|
|
661
|
+
return commented.ok ? settle(commented.said) : answer(400, { ok: false, said: commented.message });
|
|
662
|
+
}
|
|
663
|
+
if (verb === "assign") {
|
|
664
|
+
const assigned = assignFlowCard(store, target, body.get("owner") || null, who.name, now);
|
|
665
|
+
return assigned.ok ? settle(assigned.said) : answer(400, { ok: false, said: assigned.message });
|
|
666
|
+
}
|
|
667
|
+
if (verb === "watch")
|
|
668
|
+
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.");
|
|
669
|
+
if (verb === "cancel") {
|
|
670
|
+
const cancelled = cancelFlowCard(store, target, who.name, now);
|
|
671
|
+
return cancelled.ok ? settle(cancelled.said) : answer(409, { ok: false, said: cancelled.message });
|
|
672
|
+
}
|
|
673
|
+
return answer(404, { ok: false, said: "No such flow action." });
|
|
674
|
+
}
|
|
675
|
+
if (["/recipes/prepare", "/recipes/preview", "/recipes/import", "/recipes/launch", "/recipes/save"].includes(url.pathname)) {
|
|
676
|
+
const body = readForm(posted, CONSOLE_FORMS.recipes);
|
|
677
|
+
const project = projectOf(who, request);
|
|
678
|
+
try {
|
|
679
|
+
if (project === null || project === undefined || !visible(project))
|
|
680
|
+
throw new RecipeError("Open a project first.", 403);
|
|
681
|
+
for (const field of ["repo", "projectRevision", "preview", "source", "sourceRevision", "document", "recipe", "recipeRevision", "purpose", "next"])
|
|
682
|
+
if (body.getAll(field).length > 1)
|
|
683
|
+
throw new RecipeError(`Duplicated ${field} field.`);
|
|
684
|
+
if (body.get("repo") !== project || (who.via === "cookie" && body.get("projectRevision") !== String(who.session.projectRevision)))
|
|
685
|
+
throw new RecipeError("The open project changed. Reopen this recipe in the intended project.", 409);
|
|
686
|
+
const rootMode = !unscopedMode && ceiling.roots.length > 0;
|
|
687
|
+
if (rootMode && !(await authorizedProject(liveCeiling(), project)))
|
|
688
|
+
throw new RecipeError("The project is outside this server's access.", 403);
|
|
689
|
+
const rechecked = authorizeMutation(request, who, readForm(posted, CONSOLE_FORMS.mutationGuard));
|
|
690
|
+
if (rechecked !== null)
|
|
691
|
+
throw new RecipeError(rechecked.message, rechecked.status);
|
|
692
|
+
if (projectOf(who, request) !== project)
|
|
693
|
+
throw new RecipeError("The open project changed. Preview the intended project again.", 409);
|
|
694
|
+
const csrf = who.via === "cookie" ? who.session.csrf : "";
|
|
695
|
+
const revision = who.via === "cookie" ? who.session.projectRevision : 0;
|
|
696
|
+
if (url.pathname === "/recipes/prepare") {
|
|
697
|
+
const id = body.get("recipe") ?? "", version = body.get("recipeRevision") ?? "";
|
|
698
|
+
if (!/^[1-9][0-9]{0,8}$/.test(version))
|
|
699
|
+
throw new RecipeError("Choose a valid recipe version.");
|
|
700
|
+
const recipe = findRecipe(store, who.name, project, id, Number(version));
|
|
701
|
+
if (recipe === null || recipe.repo === null)
|
|
702
|
+
throw new RecipeError("This recipe is not available in this project.", 404);
|
|
703
|
+
try {
|
|
704
|
+
const preview = prepareRecipeRun(store, who.name, project, id, Number(version), recipeAnswersFromForm(body.sent), now);
|
|
705
|
+
return redirect(response, `/recipes/preview?preview=${preview.token}`);
|
|
706
|
+
}
|
|
707
|
+
catch (error) {
|
|
708
|
+
if (!(error instanceof RecipeError))
|
|
709
|
+
throw error;
|
|
710
|
+
return sendScreen(response, error.status, screen("Use recipe", recipeRunHtml(recipe, project, csrf, revision, error.message, body.sent), { chrome: chromeFor(project, "recipes"), functional: { script: recipeScript() } }));
|
|
711
|
+
}
|
|
712
|
+
}
|
|
713
|
+
if (url.pathname === "/recipes/import") {
|
|
714
|
+
const document = importRecipe(body.get("document") ?? "");
|
|
715
|
+
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() } }));
|
|
716
|
+
}
|
|
717
|
+
if (url.pathname === "/recipes/preview") {
|
|
718
|
+
const purpose = body.get("purpose") ?? "workflow";
|
|
719
|
+
if (!["recipe", "workflow"].includes(purpose))
|
|
720
|
+
throw new RecipeError("Choose preview recipe or preview workflow.");
|
|
721
|
+
const source = body.get("source") ?? "custom";
|
|
722
|
+
const sourceVersion = body.get("sourceRevision") ?? "1";
|
|
723
|
+
if (!/^[1-9][0-9]{0,8}$/.test(sourceVersion))
|
|
724
|
+
throw new RecipeError("Choose a valid recipe version.");
|
|
725
|
+
const original = ["custom", "imported", "task-copy", "routine-copy"].includes(source) ? null : findRecipe(store, who.name, project, source, Number(sourceVersion));
|
|
726
|
+
if (original === null && !["custom", "imported", "task-copy", "routine-copy"].includes(source))
|
|
727
|
+
throw new RecipeError("That recipe version is not available in this project.", 404);
|
|
728
|
+
let document;
|
|
729
|
+
try {
|
|
730
|
+
document = recipeFromForm(body.sent);
|
|
731
|
+
}
|
|
732
|
+
catch (error) {
|
|
733
|
+
if (!(error instanceof RecipeError))
|
|
734
|
+
throw error;
|
|
735
|
+
const fallback = original ?? { ...starterRecipes().find(one => one.id === "small-feature"), id: source };
|
|
736
|
+
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() } }));
|
|
737
|
+
}
|
|
738
|
+
const preview = createWorkflowPreview(store, who.name, project, document, source, now);
|
|
739
|
+
return redirect(response, `/recipes/preview?preview=${preview.token}${purpose === "recipe" || document.version === 2 ? "&purpose=recipe" : ""}`);
|
|
740
|
+
}
|
|
741
|
+
const token = body.get("preview") ?? "";
|
|
742
|
+
if (url.pathname === "/recipes/save") {
|
|
743
|
+
if (body.has("next") && body.get("next") !== "use")
|
|
744
|
+
throw new RecipeError("Choose a valid recipe destination.");
|
|
745
|
+
const saved = saveWorkflowRecipe(store, who.name, project, token, now);
|
|
746
|
+
return redirect(response, body.get("next") === "use" ? `/recipes/run?recipe=${saved.id}&revision=${saved.revision}&saved=1` : `/recipes/preview?preview=${token}`);
|
|
747
|
+
}
|
|
748
|
+
const made = launchWorkflow(store, who.name, project, token, now, who.via === "cookie");
|
|
749
|
+
if (rootMode)
|
|
750
|
+
store.upsertProject(project, projectName(project), now);
|
|
751
|
+
if (made.taskId !== null) {
|
|
752
|
+
const context = requestContext.getStore();
|
|
753
|
+
if (context !== undefined)
|
|
754
|
+
context.createdTask = made.taskId;
|
|
755
|
+
}
|
|
756
|
+
return redirect(response, made.taskId === null ? `/routines/${made.routineId}` : taskHref(made.taskId));
|
|
757
|
+
}
|
|
758
|
+
catch (error) {
|
|
759
|
+
if (error instanceof RecipeError)
|
|
760
|
+
return refuse(response, who, error.status, error.message, "/recipes");
|
|
761
|
+
throw error;
|
|
762
|
+
}
|
|
763
|
+
}
|
|
764
|
+
return refuse(response, who, 404, "There's no page at this address.", "/chat");
|
|
765
|
+
}
|
|
766
|
+
// ---- reads ---------------------------------------------------------------
|
|
767
|
+
/** A gallery template's page (Flows → New → Use this): its questions, the preview of these answers, and Create. */
|
|
768
|
+
function sendGalleryUse(response, who, template, projects, repo, given, name, notice, said = null) {
|
|
769
|
+
const answers = given ?? galleryDefaults(store, template, repo);
|
|
770
|
+
let preview = null, problem = notice;
|
|
771
|
+
try {
|
|
772
|
+
preview = previewGallery(store, template, repo, answers, who.name, clock());
|
|
773
|
+
}
|
|
774
|
+
catch (error) {
|
|
775
|
+
problem = error instanceof Error ? error.message : "That can't be made here.";
|
|
776
|
+
}
|
|
777
|
+
const diagram = preview?.built.definition ?? galleryDiagram(template);
|
|
778
|
+
const html = galleryUseHtml({ template, projects: projects.map(path => ({ path, name: projectName(path) })), repo, answers, name, preview, problem,
|
|
779
|
+
csrf: who.via === "cookie" ? who.session.csrf : "", diagram, tools: preview?.tools ?? galleryToolsOf(template, diagram, connectionsOf(store, repo)), said });
|
|
780
|
+
return sendScreen(response, problem !== null && preview === null && notice !== null ? 400 : 200, screen(template.name, `<p><a href="/flows/new">New flow</a></p><h1>${escape(template.name)}</h1>${html}`, { chrome: chromeFor(repo, "flows") }));
|
|
781
|
+
}
|
|
782
|
+
const registrations = [
|
|
783
|
+
{ id: "flows.new", domain: "flows", stage: "console", method: "GET", handle: get },
|
|
784
|
+
{ id: "flows.gallery", domain: "flows", stage: "console", method: "GET", handle: get },
|
|
785
|
+
{ id: "flows.page", domain: "flows", stage: "console", method: "GET", handle: get },
|
|
786
|
+
{ id: "kits", domain: "flows", stage: "console", method: "GET", handle: get },
|
|
787
|
+
{ id: "kit.page", domain: "flows", stage: "console", method: "GET", handle: get },
|
|
788
|
+
{ id: "teammates", domain: "flows", stage: "console", method: "GET", handle: get },
|
|
789
|
+
{ id: "teammate.page", domain: "flows", stage: "console", method: "GET", handle: get },
|
|
790
|
+
{ id: "flow.read", domain: "flows", stage: "console", method: "GET", handle: get },
|
|
791
|
+
{ id: "flow.export", domain: "flows", stage: "console", method: "GET", handle: get },
|
|
792
|
+
{ id: "flow.live", domain: "flows", stage: "console", method: "GET", handle: get },
|
|
793
|
+
{ id: "flow.page", domain: "flows", stage: "console", method: "GET", handle: get },
|
|
794
|
+
{ id: "recipes", domain: "flows", stage: "console", method: "GET", handle: get },
|
|
795
|
+
{ id: "recipes.run", domain: "flows", stage: "console", method: "GET", handle: get },
|
|
796
|
+
{ id: "recipes.start", domain: "flows", stage: "console", method: "GET", handle: get },
|
|
797
|
+
{ id: "recipes.new", domain: "flows", stage: "console", method: "GET", handle: get },
|
|
798
|
+
{ id: "recipes.edit", domain: "flows", stage: "console", method: "GET", handle: get },
|
|
799
|
+
{ id: "recipes.from-task", domain: "flows", stage: "console", method: "GET", handle: get },
|
|
800
|
+
{ id: "recipes.preview", domain: "flows", stage: "console", method: "GET", handle: get },
|
|
801
|
+
{ id: "recipes.export", domain: "flows", stage: "console", method: "GET", handle: get },
|
|
802
|
+
{ id: "recipes.other", domain: "flows", stage: "console", method: "GET", handle: get },
|
|
803
|
+
{ id: "kit.act", domain: "flows", stage: "console", method: "POST", handle: post },
|
|
804
|
+
{ id: "teammate.new", domain: "flows", stage: "console", method: "POST", handle: post },
|
|
805
|
+
{ id: "teammate.act", domain: "flows", stage: "console", method: "POST", handle: post },
|
|
806
|
+
{ id: "teammate.answer", domain: "flows", stage: "console", method: "POST", handle: post },
|
|
807
|
+
{ id: "flows.gallery-create", domain: "flows", stage: "console", method: "POST", handle: post },
|
|
808
|
+
{ id: "flows.create", domain: "flows", stage: "console", method: "POST", handle: post },
|
|
809
|
+
{ id: "flows.example", domain: "flows", stage: "console", method: "POST", handle: post },
|
|
810
|
+
{ id: "flows.import", domain: "flows", stage: "console", method: "POST", handle: post },
|
|
811
|
+
{ id: "flow.act", domain: "flows", stage: "console", method: "POST", handle: post },
|
|
812
|
+
{ id: "flow.instance-act", domain: "flows", stage: "console", method: "POST", handle: post },
|
|
813
|
+
{ id: "flow.triggers", domain: "flows", stage: "console", method: "POST", handle: post },
|
|
814
|
+
{ id: "flow.card", domain: "flows", stage: "console", method: "POST", handle: post },
|
|
815
|
+
{ id: "flow.card-choose", domain: "flows", stage: "console", method: "POST", handle: post },
|
|
816
|
+
{ id: "flow.trigger.pause", domain: "flows", stage: "console", method: "POST", handle: post },
|
|
817
|
+
{ id: "flow.trigger.resume", domain: "flows", stage: "console", method: "POST", handle: post },
|
|
818
|
+
{ id: "flow.trigger.remove", domain: "flows", stage: "console", method: "POST", handle: post },
|
|
819
|
+
{ id: "flow.trigger.check", domain: "flows", stage: "console", method: "POST", handle: post },
|
|
820
|
+
{ id: "flow.trigger.press", domain: "flows", stage: "console", method: "POST", handle: post },
|
|
821
|
+
{ id: "flow.trigger.renew", domain: "flows", stage: "console", method: "POST", handle: post },
|
|
822
|
+
{ id: "flow.trigger.secret", domain: "flows", stage: "console", method: "POST", handle: post },
|
|
823
|
+
{ id: "flow.trigger.share", domain: "flows", stage: "console", method: "POST", handle: post },
|
|
824
|
+
{ id: "flow.trigger.unshare", domain: "flows", stage: "console", method: "POST", handle: post },
|
|
825
|
+
{ id: "recipes.prepare-send", domain: "flows", stage: "console", method: "POST", handle: post },
|
|
826
|
+
{ id: "recipes.preview-send", domain: "flows", stage: "console", method: "POST", handle: post },
|
|
827
|
+
{ id: "recipes.import-send", domain: "flows", stage: "console", method: "POST", handle: post },
|
|
828
|
+
{ id: "recipes.save-send", domain: "flows", stage: "console", method: "POST", handle: post },
|
|
829
|
+
{ id: "recipes.launch-send", domain: "flows", stage: "console", method: "POST", handle: post },
|
|
830
|
+
];
|
|
831
|
+
return { registrations, get, post, sendGalleryUse };
|
|
832
|
+
}
|