toolroll 0.9.34 → 0.9.37
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 +102 -0
- package/README.md +7 -0
- package/dist/assignment-adapters.d.ts +12 -48
- package/dist/assignment-adapters.js +33 -43
- package/dist/assignment.d.ts +1 -0
- package/dist/assignment.js +2 -2
- package/dist/browser-workspace.d.ts +3 -3
- package/dist/build-review.d.ts +1 -1
- package/dist/build-review.js +2 -1
- package/dist/builder.js +4 -7
- package/dist/chat-acceptance.d.ts +2 -2
- package/dist/chat-actions.d.ts +18 -12
- package/dist/chat-actions.js +15 -65
- package/dist/chat-ask.js +5 -5
- package/dist/chat-decide.d.ts +14 -1
- package/dist/chat-decide.js +12 -6
- package/dist/chat-delivery-state.d.ts +12 -70
- package/dist/chat-delivery-state.js +17 -135
- package/dist/chat-delivery.js +58 -30
- package/dist/chat-flow.js +8 -6
- package/dist/chat-inbox.js +1 -1
- package/dist/coding-context.d.ts +2 -39
- package/dist/coding-context.js +2 -1
- package/dist/coding-handoff.d.ts +6 -25
- package/dist/coding-handoff.js +11 -5
- package/dist/coding-types.d.ts +2 -26
- package/dist/coding-workspace.d.ts +2 -0
- package/dist/coding-workspace.js +50 -14
- package/dist/contracts/acceptance-terms.d.ts +14 -0
- package/dist/contracts/acceptance-terms.js +16 -0
- package/dist/contracts/chat-actions.d.ts +328 -0
- package/dist/contracts/chat-actions.js +133 -0
- package/dist/contracts/chat-callback-rows.d.ts +59 -0
- package/dist/contracts/chat-callback-rows.js +46 -0
- package/dist/contracts/chat-content.d.ts +276 -0
- package/dist/contracts/chat-content.js +127 -0
- package/dist/contracts/chat-tables.d.ts +6 -0
- package/dist/contracts/chat-tables.js +136 -0
- package/dist/contracts/coding-context.d.ts +97 -0
- package/dist/contracts/coding-context.js +60 -0
- package/dist/contracts/coding-handoff.d.ts +44 -0
- package/dist/contracts/coding-handoff.js +47 -0
- package/dist/contracts/coding-workspace.d.ts +165 -0
- package/dist/contracts/coding-workspace.js +71 -0
- package/dist/contracts/contract.d.ts +40 -7
- package/dist/contracts/contract.js +155 -19
- package/dist/contracts/decision.d.ts +45 -0
- package/dist/contracts/decision.js +45 -0
- package/dist/contracts/discord-callback.d.ts +45 -0
- package/dist/contracts/discord-callback.js +34 -0
- package/dist/contracts/flow-card.d.ts +50 -0
- package/dist/contracts/flow-card.js +46 -0
- package/dist/contracts/flow-propose.d.ts +325 -0
- package/dist/contracts/flow-propose.js +41 -0
- package/dist/contracts/flow-send.d.ts +119 -0
- package/dist/contracts/flow-send.js +112 -0
- package/dist/contracts/flow.d.ts +19405 -0
- package/dist/contracts/flow.js +364 -0
- package/dist/contracts/gateway-tools.d.ts +374 -0
- package/dist/contracts/gateway-tools.js +94 -0
- package/dist/contracts/handoff.d.ts +91 -0
- package/dist/contracts/handoff.js +82 -0
- package/dist/contracts/lead-context.d.ts +165 -0
- package/dist/contracts/lead-context.js +73 -0
- package/dist/contracts/lead-tools.d.ts +1905 -0
- package/dist/contracts/lead-tools.js +265 -0
- package/dist/contracts/memory-pass.d.ts +158 -0
- package/dist/contracts/memory-pass.js +102 -0
- package/dist/contracts/project-knowledge.d.ts +126 -0
- package/dist/contracts/project-knowledge.js +93 -0
- package/dist/contracts/project-memory.d.ts +117 -0
- package/dist/contracts/project-memory.js +51 -0
- package/dist/contracts/project-skills.d.ts +121 -0
- package/dist/contracts/project-skills.js +76 -0
- package/dist/contracts/proof.d.ts +136 -0
- package/dist/contracts/proof.js +110 -0
- package/dist/contracts/review-findings.d.ts +46 -0
- package/dist/contracts/review-findings.js +48 -0
- package/dist/contracts/route.d.ts +484 -0
- package/dist/contracts/route.js +128 -0
- package/dist/contracts/scope.d.ts +105 -0
- package/dist/contracts/scope.js +114 -0
- package/dist/contracts/scout-report.d.ts +234 -0
- package/dist/contracts/scout-report.js +141 -0
- package/dist/contracts/slack-callback.d.ts +43 -0
- package/dist/contracts/slack-callback.js +35 -0
- package/dist/contracts/sort-answer.d.ts +83 -0
- package/dist/contracts/sort-answer.js +77 -0
- package/dist/contracts/stage-output.d.ts +127 -0
- package/dist/contracts/stage-output.js +166 -0
- package/dist/contracts/task-sizing.d.ts +24 -0
- package/dist/contracts/task-sizing.js +28 -0
- package/dist/contracts/teammate-turn.d.ts +43 -0
- package/dist/contracts/teammate-turn.js +51 -0
- package/dist/contracts/teams-callback.d.ts +14 -0
- package/dist/contracts/teams-callback.js +14 -0
- package/dist/contracts/telegram-callback.d.ts +120 -0
- package/dist/contracts/telegram-callback.js +97 -0
- package/dist/contracts/verification-receipt.d.ts +148 -0
- package/dist/contracts/verification-receipt.js +87 -0
- package/dist/decision.d.ts +11 -20
- package/dist/decision.js +86 -167
- package/dist/demo.js +28 -11
- package/dist/discord-chat.js +31 -20
- package/dist/evidence.d.ts +3 -40
- package/dist/evidence.js +5 -1
- package/dist/flow-engine.js +14 -17
- package/dist/flow-gallery.d.ts +3 -2
- package/dist/flow-items.d.ts +4 -13
- package/dist/flow-items.js +1 -15
- package/dist/flow-send.d.ts +6 -37
- package/dist/flow-send.js +18 -14
- package/dist/flow-share.d.ts +10 -32
- package/dist/flow-share.js +72 -84
- package/dist/flow-sort.d.ts +7 -26
- package/dist/flow-sort.js +25 -20
- package/dist/flow-starters.d.ts +2 -1
- package/dist/flow-steps.js +3 -2
- package/dist/flow-triggers.d.ts +15 -72
- package/dist/flow-triggers.js +94 -83
- package/dist/flows.d.ts +37 -220
- package/dist/flows.js +356 -387
- package/dist/invoke.js +4 -4
- package/dist/lead-context.d.ts +4 -2
- package/dist/lead-context.js +24 -17
- package/dist/mate-tools.d.ts +4 -0
- package/dist/mate-tools.js +111 -308
- package/dist/mcp-connect.d.ts +27 -4
- package/dist/mcp-connect.js +92 -17
- package/dist/mcp.js +84 -216
- package/dist/memory-pass.d.ts +17 -42
- package/dist/memory-pass.js +54 -26
- package/dist/operate.js +31 -2
- package/dist/phase-routing.d.ts +19 -72
- package/dist/phase-routing.js +34 -164
- package/dist/plan.d.ts +5 -5
- package/dist/plan.js +5 -25
- package/dist/process-recovery-eligibility.d.ts +2 -2
- package/dist/process-recovery-eligibility.js +13 -8
- package/dist/project-knowledge.d.ts +16 -35
- package/dist/project-knowledge.js +42 -21
- package/dist/project-memory.d.ts +2 -28
- package/dist/project-memory.js +28 -13
- package/dist/project-skills.d.ts +16 -38
- package/dist/project-skills.js +41 -32
- package/dist/project-tools.d.ts +1 -0
- package/dist/project-tools.js +4 -4
- package/dist/proof.d.ts +15 -91
- package/dist/proof.js +90 -249
- package/dist/provider.d.ts +2 -0
- package/dist/provider.js +4 -16
- package/dist/result-evidence-readers.d.ts +42 -0
- package/dist/result-evidence-readers.js +91 -0
- package/dist/result-shots.d.ts +1 -1
- package/dist/result-shots.js +2 -1
- package/dist/review-switch.d.ts +3 -8
- package/dist/reviewer.d.ts +8 -12
- package/dist/reviewer.js +11 -35
- package/dist/scope.d.ts +21 -43
- package/dist/scope.js +42 -128
- package/dist/scout-report.d.ts +9 -194
- package/dist/scout-report.js +113 -264
- package/dist/scout.js +15 -11
- package/dist/serve.js +17 -101
- package/dist/slack-chat.js +26 -20
- package/dist/slack-state.js +2 -1
- package/dist/store.d.ts +9 -32
- package/dist/store.js +18 -15
- package/dist/structured-output.js +2 -1
- package/dist/subscription-chat.d.ts +4 -0
- package/dist/subscription-chat.js +4 -1
- package/dist/surface.js +8 -1
- package/dist/task-review-brief.d.ts +69 -0
- package/dist/task-review-brief.js +179 -0
- package/dist/task-sizing.d.ts +3 -27
- package/dist/task-sizing.js +11 -26
- package/dist/task-text.d.ts +0 -5
- package/dist/task-text.js +1 -3
- package/dist/teammate-question.js +7 -5
- package/dist/teammate-work.js +9 -5
- package/dist/teammates.d.ts +13 -64
- package/dist/teammates.js +32 -33
- package/dist/teams-chat.js +20 -11
- package/dist/telegram-flow.js +4 -3
- package/dist/telegram-mate.d.ts +4 -14
- package/dist/telegram-mate.js +8 -7
- package/dist/telegram-team.d.ts +2 -19
- package/dist/telegram.d.ts +1 -1
- package/dist/telegram.js +52 -42
- package/dist/text-limits.d.ts +212 -1
- package/dist/text-limits.js +214 -1
- package/dist/tools-ui.d.ts +2 -1
- package/dist/tools-ui.js +7 -4
- package/dist/verification-evidence.js +24 -12
- package/package.json +1 -1
package/dist/flow-triggers.d.ts
CHANGED
|
@@ -1,77 +1,12 @@
|
|
|
1
1
|
import type { Runner } from "./backend.js";
|
|
2
2
|
import { type FlowAct } from "./flow-engine.js";
|
|
3
|
-
import type
|
|
3
|
+
import { type FlowDefinition } from "./flows.js";
|
|
4
|
+
import { type ContractResult } from "./contracts/contract.js";
|
|
5
|
+
import { FLOW_TRIGGER_KINDS, type ChatApp, type FlowTriggerKind, type TriggerConfig, type TriggerInput } from "./contracts/flow.js";
|
|
4
6
|
import { type MailReader } from "./mailbox.js";
|
|
5
7
|
import type { FlowCardSource, FlowRow, FlowTriggerRow, Store } from "./store.js";
|
|
6
|
-
export
|
|
7
|
-
export type FlowTriggerKind
|
|
8
|
-
export type TriggerConfig = {
|
|
9
|
-
kind: "button";
|
|
10
|
-
label: string;
|
|
11
|
-
questions: string[];
|
|
12
|
-
zone: string | null;
|
|
13
|
-
}
|
|
14
|
-
/** `script` (v90): run this project script on the schedule and make a card of each item it prints, instead of one card titled `title`. */
|
|
15
|
-
| {
|
|
16
|
-
kind: "schedule";
|
|
17
|
-
schedule: string;
|
|
18
|
-
title: string;
|
|
19
|
-
description: string | null;
|
|
20
|
-
zone: string | null;
|
|
21
|
-
script?: string;
|
|
22
|
-
secrets?: string[];
|
|
23
|
-
} | {
|
|
24
|
-
kind: "github";
|
|
25
|
-
repo: string;
|
|
26
|
-
watch: "issues" | "pulls" | "checks";
|
|
27
|
-
label: string | null;
|
|
28
|
-
branch: string | null;
|
|
29
|
-
from: "team" | "anyone";
|
|
30
|
-
delivery: "poll" | "webhook";
|
|
31
|
-
zone: string | null;
|
|
32
|
-
} | {
|
|
33
|
-
kind: "linear";
|
|
34
|
-
team: string | null;
|
|
35
|
-
state: string | null;
|
|
36
|
-
label: string | null;
|
|
37
|
-
delivery: "poll" | "webhook";
|
|
38
|
-
zone: string | null;
|
|
39
|
-
} | {
|
|
40
|
-
kind: "flow";
|
|
41
|
-
flow: number;
|
|
42
|
-
when: string;
|
|
43
|
-
zone: string | null;
|
|
44
|
-
} | {
|
|
45
|
-
kind: "webhook";
|
|
46
|
-
title: string;
|
|
47
|
-
titleField: string | null;
|
|
48
|
-
bodyField: string | null;
|
|
49
|
-
zone: string | null;
|
|
50
|
-
}
|
|
51
|
-
/** `sender`: addresses or domains, comma-separated; `subject`: words the subject must contain. */
|
|
52
|
-
| {
|
|
53
|
-
kind: "email";
|
|
54
|
-
folder: string;
|
|
55
|
-
sender: string | null;
|
|
56
|
-
subject: string | null;
|
|
57
|
-
zone: string | null;
|
|
58
|
-
}
|
|
59
|
-
/** A channel in a chat app; `binding` is the pairing of the person who connected it (whose chat answers there). */
|
|
60
|
-
| {
|
|
61
|
-
kind: "chat";
|
|
62
|
-
app: ChatApp;
|
|
63
|
-
installation: string;
|
|
64
|
-
chat: string;
|
|
65
|
-
binding: number;
|
|
66
|
-
zone: string | null;
|
|
67
|
-
}
|
|
68
|
-
/** `schedule`: always daily, in the routines' form ("daily:07:30@Europe/London"). */
|
|
69
|
-
| {
|
|
70
|
-
kind: "plane-review";
|
|
71
|
-
schedule: string;
|
|
72
|
-
zone: string | null;
|
|
73
|
-
};
|
|
74
|
-
export type ChatApp = "slack" | "discord" | "teams" | "telegram";
|
|
8
|
+
export { FLOW_TRIGGER_KINDS };
|
|
9
|
+
export type { ChatApp, FlowTriggerKind, TriggerConfig, TriggerInput };
|
|
75
10
|
export declare const CHAT_APP_NAMES: Record<ChatApp, string>;
|
|
76
11
|
/** What each kind is called on the canvas. */
|
|
77
12
|
export declare const FLOW_TRIGGER_WORDS: Record<FlowTriggerKind, string>;
|
|
@@ -79,11 +14,18 @@ export declare const FLOW_TRIGGER_WORDS: Record<FlowTriggerKind, string>;
|
|
|
79
14
|
export declare const POLL_EVERY_MS: number;
|
|
80
15
|
/** At most this many cards from one check or one delivery; the rest wait for the next. */
|
|
81
16
|
export declare const CARDS_PER_CHECK = 10;
|
|
17
|
+
/**
|
|
18
|
+
* A trigger's settings read by their one schema (`label: required`, `unknown key 'flows'`). A setting given as null is
|
|
19
|
+
* left out, and a button's blank question lines are dropped, as every release has read them. Then what the schema can't
|
|
20
|
+
* say, named by its path: no hidden characters and nothing that looks like a key, anywhere in the settings.
|
|
21
|
+
*/
|
|
22
|
+
export declare function readTriggerSettings(raw: unknown): ContractResult<TriggerInput>;
|
|
82
23
|
/** A schedule said in words ("every 2 hours", "daily 09:00 Europe/London", "monday 09:00") or in the routines' own form. */
|
|
83
24
|
export declare function scheduleFromWords(text: string): string | null;
|
|
84
25
|
/** A GitHub repository as owner/name, read from a project's origin remote when it is on GitHub. */
|
|
85
26
|
export declare function githubRepoOf(path: string): string | null;
|
|
86
|
-
/** A trigger's settings, checked whole against the flow it starts cards in
|
|
27
|
+
/** A trigger's settings, checked whole against the flow it starts cards in (readTriggerSettings, then the flow, its zones,
|
|
28
|
+
* the project's scripts and the schedule). Throws FlowContractError, each line naming its setting. */
|
|
87
29
|
export declare function validateTriggerConfig(raw: unknown, context: {
|
|
88
30
|
store: Store;
|
|
89
31
|
flow: FlowRow;
|
|
@@ -105,6 +47,8 @@ export declare function triggerHeadline(config: TriggerConfig, store: Store): {
|
|
|
105
47
|
detail: string;
|
|
106
48
|
};
|
|
107
49
|
export declare function triggerConfigOf(trigger: FlowTriggerRow): TriggerConfig | null;
|
|
50
|
+
/** A saved trigger's config (`configJson`, which carries no version) read by its one schema, what it leaves out filled. */
|
|
51
|
+
export declare function readTriggerConfig(raw: unknown): ContractResult<TriggerConfig>;
|
|
108
52
|
/** Whether this trigger takes deliveries at a secret address rather than being checked. */
|
|
109
53
|
export declare function takesDeliveries(config: TriggerConfig): boolean;
|
|
110
54
|
/** A webhook's signing secret (GitHub: made here; Linear: pasted from Linear). 0600, beside the database, never a column. */
|
|
@@ -267,4 +211,3 @@ export type FormAnswer = {
|
|
|
267
211
|
export declare function flowFormPage(store: Store, token: string, now: Date): FormAnswer;
|
|
268
212
|
/** POST a shared form: at most one card per submission, keys refused, a limit per hour, and quiet about what a bot did wrong. */
|
|
269
213
|
export declare function receiveFlowForm(store: Store, token: string, fields: URLSearchParams, now: Date): FormAnswer;
|
|
270
|
-
export {};
|
package/dist/flow-triggers.js
CHANGED
|
@@ -33,13 +33,16 @@ import { chmodSync, existsSync, mkdirSync, readFileSync, rmSync, statSync, write
|
|
|
33
33
|
import { join } from "node:path";
|
|
34
34
|
import { scanForSecrets } from "./evidence.js";
|
|
35
35
|
import { flowCardText, flowDefinitionOf } from "./flow-engine.js";
|
|
36
|
+
import { FlowContractError } from "./flows.js";
|
|
37
|
+
import { parseContract } from "./contracts/contract.js";
|
|
38
|
+
import { FLOW_ALIASES, FLOW_TRIGGER_KINDS, SECRETS_MAX, triggerConfigSchema, triggerInputSchema } from "./contracts/flow.js";
|
|
36
39
|
import { describeSchedule, firstFireAt, nextFireAt, parseSchedule, WEEKDAYS } from "./routine.js";
|
|
37
40
|
import { takeReply } from "./flow-replies.js";
|
|
38
41
|
import { mailboxAccess, mailCursorOf, mailCursorText, readThroughImap } from "./mailbox.js";
|
|
39
42
|
import { readFlowSecrets } from "./flow-secrets.js";
|
|
40
43
|
import { tmpdir } from "node:os";
|
|
41
44
|
import { envValue } from "./names.js";
|
|
42
|
-
export
|
|
45
|
+
export { FLOW_TRIGGER_KINDS };
|
|
43
46
|
export const CHAT_APP_NAMES = { slack: "Slack", discord: "Discord", teams: "Teams", telegram: "Telegram" };
|
|
44
47
|
/** What each kind is called on the canvas. */
|
|
45
48
|
export const FLOW_TRIGGER_WORDS = {
|
|
@@ -54,23 +57,30 @@ const HOOK_BYTES = 1_000_000;
|
|
|
54
57
|
/** Authors GitHub says have write access to the repository. */
|
|
55
58
|
const TEAM = new Set(["OWNER", "MEMBER", "COLLABORATOR"]);
|
|
56
59
|
// ------------------------------------------------------------ the terms
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
60
|
+
const HIDDEN = /[\u0000-\u0008\u000b\u000c\u000e-\u001f\u007f\u200b-\u200f\u202a-\u202e\u2066-\u2069]/;
|
|
61
|
+
const isRecord = (value) => typeof value === "object" && value !== null && !Array.isArray(value);
|
|
62
|
+
/**
|
|
63
|
+
* A trigger's settings read by their one schema (`label: required`, `unknown key 'flows'`). A setting given as null is
|
|
64
|
+
* left out, and a button's blank question lines are dropped, as every release has read them. Then what the schema can't
|
|
65
|
+
* say, named by its path: no hidden characters and nothing that looks like a key, anywhere in the settings.
|
|
66
|
+
*/
|
|
67
|
+
export function readTriggerSettings(raw) {
|
|
68
|
+
const body = isRecord(raw) ? Object.fromEntries(Object.entries(raw).filter(([, value]) => value !== null).map(([key, value]) => [key, key === "questions" && Array.isArray(value) ? value.filter(one => typeof one !== "string" || one.trim() !== "") : value])) : raw;
|
|
69
|
+
const read = parseContract(triggerInputSchema, body, FLOW_ALIASES);
|
|
70
|
+
if (!read.ok)
|
|
71
|
+
return read;
|
|
72
|
+
for (const [key, value] of Object.entries(read.value)) {
|
|
73
|
+
for (const [n, one] of (Array.isArray(value) ? value : [value]).entries()) {
|
|
74
|
+
if (typeof one !== "string")
|
|
75
|
+
continue;
|
|
76
|
+
const path = Array.isArray(value) ? `${key}[${n}]` : key;
|
|
77
|
+
if (HIDDEN.test(one))
|
|
78
|
+
return { ok: false, issues: [{ path, kind: "bad-value", line: `${path}: can't contain hidden characters` }] };
|
|
79
|
+
if (scanForSecrets(one).length > 0)
|
|
80
|
+
return { ok: false, issues: [{ path, kind: "bad-value", line: `${path}: looks like a key or password; keys never go in a trigger's settings` }] };
|
|
81
|
+
}
|
|
63
82
|
}
|
|
64
|
-
|
|
65
|
-
throw new Error(`The trigger's ${key} must be plain text.`);
|
|
66
|
-
const trimmed = value.trim();
|
|
67
|
-
if (trimmed.length > cap)
|
|
68
|
-
throw new Error(`The trigger's ${key} is up to ${cap} characters.`);
|
|
69
|
-
if (/[\u0000-\u0008\u000b\u000c\u000e-\u001f\u007f\u200b-\u200f\u202a-\u202e\u2066-\u2069]/.test(trimmed))
|
|
70
|
-
throw new Error(`The trigger's ${key} can't contain hidden characters.`);
|
|
71
|
-
if (scanForSecrets(trimmed).length > 0)
|
|
72
|
-
throw new Error("That looks like a key or password. Keys never go in a trigger's settings.");
|
|
73
|
-
return trimmed;
|
|
83
|
+
return read;
|
|
74
84
|
}
|
|
75
85
|
/** A schedule said in words ("every 2 hours", "daily 09:00 Europe/London", "monday 09:00") or in the routines' own form. */
|
|
76
86
|
export function scheduleFromWords(text) {
|
|
@@ -116,111 +126,98 @@ export function githubRepoOf(path) {
|
|
|
116
126
|
return null;
|
|
117
127
|
}
|
|
118
128
|
}
|
|
119
|
-
/** A trigger's settings, checked whole against the flow it starts cards in
|
|
129
|
+
/** A trigger's settings, checked whole against the flow it starts cards in (readTriggerSettings, then the flow, its zones,
|
|
130
|
+
* the project's scripts and the schedule). Throws FlowContractError, each line naming its setting. */
|
|
120
131
|
export function validateTriggerConfig(raw, context) {
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
const
|
|
125
|
-
if (!
|
|
126
|
-
throw new
|
|
127
|
-
const
|
|
132
|
+
// Connecting a channel proves it's one the person is in: it happens in the channel, not here.
|
|
133
|
+
if (isRecord(raw) && raw["kind"] === "chat")
|
|
134
|
+
throw new FlowContractError([`kind: connect a chat channel from the channel itself: where Toolroll is in Slack, Discord, Teams or a Telegram group, send “flow ${context.flow.id}”`]);
|
|
135
|
+
const read = readTriggerSettings(raw);
|
|
136
|
+
if (!read.ok)
|
|
137
|
+
throw new FlowContractError(read.issues.map(one => one.line));
|
|
138
|
+
const input = read.value;
|
|
139
|
+
const refuse = (path, what) => { throw new FlowContractError([`${path}: ${what}`]); };
|
|
140
|
+
/** A setting's words, trimmed; blank is left out. */
|
|
141
|
+
const said = (value) => value === undefined || value.trim() === "" ? null : value.trim();
|
|
142
|
+
const named = said(input.zone);
|
|
128
143
|
const zoneStage = named === null ? null : context.definition.stages.find(one => one.id === named || one.title.toLowerCase() === named.toLowerCase());
|
|
129
144
|
if (named !== null && zoneStage === undefined)
|
|
130
|
-
|
|
145
|
+
refuse("zone", `this flow has no zone called ${named}`);
|
|
131
146
|
const zone = zoneStage?.id ?? null;
|
|
132
|
-
|
|
133
|
-
const value = input[key];
|
|
134
|
-
if (value === undefined || value === null || value === "")
|
|
135
|
-
return fallback;
|
|
136
|
-
if (!options.includes(value))
|
|
137
|
-
throw new Error(`Choose the trigger's ${key}: ${options.join(" or ")}.`);
|
|
138
|
-
return value;
|
|
139
|
-
};
|
|
140
|
-
switch (kind) {
|
|
147
|
+
switch (input.kind) {
|
|
141
148
|
case "button": {
|
|
142
|
-
const label =
|
|
143
|
-
const
|
|
144
|
-
const questions = (Array.isArray(asked) ? asked : typeof asked === "string" ? asked.split("\n") : [])
|
|
145
|
-
.map(one => typeof one === "string" ? one.trim() : "").filter(one => one !== "");
|
|
149
|
+
const label = said(input.label) ?? refuse("label", "required");
|
|
150
|
+
const questions = (Array.isArray(input.questions) ? input.questions : typeof input.questions === "string" ? input.questions.split("\n") : []).map(one => one.trim()).filter(one => one !== "");
|
|
146
151
|
if (questions.length > 6)
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
throw new Error("Each question is a short line of plain text.");
|
|
152
|
+
refuse("questions", "at most 6 items");
|
|
153
|
+
questions.forEach((question, n) => { if (question.length > 80)
|
|
154
|
+
refuse(`questions[${n}]`, "at most 80 characters"); });
|
|
151
155
|
return { kind: "button", label, questions: questions.length === 0 ? ["What needs doing?", "Details"] : questions, zone };
|
|
152
156
|
}
|
|
153
157
|
case "schedule": {
|
|
154
|
-
const
|
|
155
|
-
const schedule = scheduleFromWords(
|
|
156
|
-
if (schedule === null)
|
|
157
|
-
throw new Error("Say the schedule like “every 2 hours”, “daily 09:00 Europe/London”, “weekdays 09:00” or “monday 09:00”.");
|
|
158
|
+
const said_ = said(input.schedule) ?? refuse("schedule", "required");
|
|
159
|
+
const schedule = scheduleFromWords(said_) ?? refuse("schedule", "say it like “every 2 hours”, “daily 09:00 Europe/London”, “weekdays 09:00” or “monday 09:00”");
|
|
158
160
|
// A script on a schedule (v90): each item it prints becomes a card.
|
|
159
|
-
const script =
|
|
161
|
+
const script = said(input.script);
|
|
160
162
|
if (script !== null) {
|
|
161
163
|
if (context.store.flowScript(context.flow.repo, script) === null)
|
|
162
|
-
|
|
163
|
-
const
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
throw new Error("Name up to 10 saved secrets in capitals, like API_TOKEN.");
|
|
164
|
+
refuse("script", `there's no script called ${script} in this project; make it on the flow's Scripts panel first`);
|
|
165
|
+
const secrets = input.secrets === undefined ? [] : Array.isArray(input.secrets) ? input.secrets : input.secrets.split(/[\s,]+/).filter(one => one !== "");
|
|
166
|
+
if (secrets.length > SECRETS_MAX || secrets.some(one => !/^[A-Z][A-Z0-9_]{0,39}$/.test(one)))
|
|
167
|
+
refuse("secrets", "name up to 10 saved secrets in capitals, like API_TOKEN");
|
|
167
168
|
return { kind: "schedule", schedule, title: `Items from ${script}`, description: null, zone, script, ...(secrets.length === 0 ? {} : { secrets: [...new Set(secrets)] }) };
|
|
168
169
|
}
|
|
169
|
-
return { kind: "schedule", schedule, title:
|
|
170
|
+
return { kind: "schedule", schedule, title: said(input.title) ?? refuse("title", "required"), description: said(input.description), zone };
|
|
170
171
|
}
|
|
171
172
|
case "github": {
|
|
172
|
-
const repo =
|
|
173
|
+
const repo = said(input.repo) ?? githubRepoOf(context.flow.repo);
|
|
173
174
|
if (repo === null || !/^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/.test(repo))
|
|
174
|
-
|
|
175
|
-
const watch =
|
|
176
|
-
const label = watch === "checks" ? null :
|
|
177
|
-
const branch = watch === "checks" ?
|
|
175
|
+
refuse("repo", "name the GitHub repository as owner/name");
|
|
176
|
+
const watch = input.watch ?? "issues";
|
|
177
|
+
const label = watch === "checks" ? null : said(input.label);
|
|
178
|
+
const branch = watch === "checks" ? said(input.branch) ?? "main" : null;
|
|
178
179
|
if (branch !== null && !/^[A-Za-z0-9._/-]+$/.test(branch))
|
|
179
|
-
|
|
180
|
-
return { kind: "github", repo, watch, label, branch, from:
|
|
180
|
+
refuse("branch", "that branch name isn't valid");
|
|
181
|
+
return { kind: "github", repo: repo, watch, label, branch, from: input.from ?? "team", delivery: input.delivery ?? "poll", zone };
|
|
181
182
|
}
|
|
182
183
|
case "linear": {
|
|
183
|
-
const team =
|
|
184
|
+
const team = said(input.team), state = said(input.state), label = said(input.label);
|
|
184
185
|
if (team !== null && !/^[A-Za-z0-9]+$/.test(team))
|
|
185
|
-
|
|
186
|
+
refuse("team", "a Linear team is its short key, like ENG");
|
|
186
187
|
if (team === null && label === null)
|
|
187
|
-
|
|
188
|
-
return { kind: "linear", team: team?.toUpperCase() ?? null, state, label, delivery:
|
|
188
|
+
refuse("team", "name a Linear team or a label, so the trigger doesn't take every issue in the workspace");
|
|
189
|
+
return { kind: "linear", team: team?.toUpperCase() ?? null, state, label, delivery: input.delivery ?? "poll", zone };
|
|
189
190
|
}
|
|
190
191
|
case "flow": {
|
|
191
|
-
const
|
|
192
|
-
const flow = Number.isSafeInteger(source) ? context.store.getFlow(source) : null;
|
|
192
|
+
const flow = context.store.getFlow(input.flow);
|
|
193
193
|
const sourceDefinition = flow === null ? null : flowDefinitionOf(flow);
|
|
194
194
|
if (flow === null || flow.state !== "active" || sourceDefinition === null || !context.store.accountCanAccess(context.actor, flow.repo))
|
|
195
|
-
|
|
195
|
+
return refuse("flow", "choose another flow in your projects");
|
|
196
196
|
if (flow.id === context.flow.id)
|
|
197
|
-
|
|
198
|
-
const when =
|
|
197
|
+
refuse("flow", "a flow can't start cards in itself; move them with a zone's next step instead");
|
|
198
|
+
const when = said(input.when);
|
|
199
199
|
const stage = when === null ? sourceDefinition.stages.find(one => one.kind === "done") : sourceDefinition.stages.find(one => one.id === when || one.title.toLowerCase() === when.toLowerCase());
|
|
200
200
|
if (stage === undefined)
|
|
201
|
-
|
|
201
|
+
return refuse("when", `${flow.name} has no zone called ${when ?? "Done"}`);
|
|
202
202
|
return { kind: "flow", flow: flow.id, when: stage.id, zone };
|
|
203
203
|
}
|
|
204
204
|
case "webhook":
|
|
205
|
-
return { kind: "webhook", title:
|
|
206
|
-
case "chat":
|
|
207
|
-
// Connecting a channel proves it's one the person is in: it happens in the channel, not here.
|
|
208
|
-
throw new Error(`Connect a chat channel from the channel itself: where Toolroll is in Slack, Discord, Teams or a Telegram group, send “flow ${context.flow.id}”.`);
|
|
205
|
+
return { kind: "webhook", title: said(input.title) ?? "Webhook", titleField: said(input.titleField), bodyField: said(input.bodyField), zone };
|
|
209
206
|
case "plane-review": {
|
|
210
|
-
const at =
|
|
207
|
+
const at = said(input.at) ?? "07:30";
|
|
211
208
|
const clock = /^(\d{1,2}):(\d{2})$/.exec(at);
|
|
212
|
-
const zoneName =
|
|
209
|
+
const zoneName = said(input.timeZone) ?? localTimeZone();
|
|
213
210
|
const schedule = clock === null ? null : scheduleFromWords(`daily ${clock[1]}:${clock[2]} ${zoneName}`);
|
|
214
211
|
if (schedule === null)
|
|
215
|
-
|
|
212
|
+
return refuse("at", "say the time it reviews the day as HH:MM, like 07:30, and a time zone like Europe/London");
|
|
216
213
|
return { kind: "plane-review", schedule, zone };
|
|
217
214
|
}
|
|
218
215
|
case "email": {
|
|
219
|
-
const folder =
|
|
220
|
-
const sender =
|
|
216
|
+
const folder = said(input.folder) ?? "INBOX";
|
|
217
|
+
const sender = said(input.sender);
|
|
221
218
|
if (sender !== null && sendersOf(sender).length === 0)
|
|
222
|
-
|
|
223
|
-
return { kind: "email", folder: /^inbox$/i.test(folder) ? "INBOX" : folder, sender: sender === null ? null : sendersOf(sender).join(", "), subject:
|
|
219
|
+
refuse("sender", "say whom mail comes from as addresses or domains, like priya@example.com or example.com");
|
|
220
|
+
return { kind: "email", folder: /^inbox$/i.test(folder) ? "INBOX" : folder, sender: sender === null ? null : sendersOf(sender).join(", "), subject: said(input.subject), zone };
|
|
224
221
|
}
|
|
225
222
|
}
|
|
226
223
|
}
|
|
@@ -300,12 +297,26 @@ export function triggerHeadline(config, store) {
|
|
|
300
297
|
}
|
|
301
298
|
}
|
|
302
299
|
export function triggerConfigOf(trigger) {
|
|
300
|
+
let raw;
|
|
303
301
|
try {
|
|
304
|
-
|
|
302
|
+
raw = JSON.parse(trigger.configJson);
|
|
305
303
|
}
|
|
306
304
|
catch {
|
|
307
305
|
return null;
|
|
308
306
|
}
|
|
307
|
+
const read = readTriggerConfig(raw);
|
|
308
|
+
return read.ok ? read.value : null;
|
|
309
|
+
}
|
|
310
|
+
/** What a saved trigger leaves out, it had as these (every release has saved them whole; a hand-made row may not). */
|
|
311
|
+
const SAVED_DEFAULTS = {
|
|
312
|
+
button: { questions: ["What needs doing?", "Details"] }, schedule: { description: null }, github: { label: null, branch: null, from: "team", delivery: "poll" },
|
|
313
|
+
linear: { team: null, state: null, label: null, delivery: "poll" }, webhook: { title: "Webhook", titleField: null, bodyField: null }, email: { folder: "INBOX", sender: null, subject: null },
|
|
314
|
+
};
|
|
315
|
+
/** A saved trigger's config (`configJson`, which carries no version) read by its one schema, what it leaves out filled. */
|
|
316
|
+
export function readTriggerConfig(raw) {
|
|
317
|
+
const kind = isRecord(raw) ? raw["kind"] : undefined;
|
|
318
|
+
const body = isRecord(raw) ? { zone: null, ...SAVED_DEFAULTS[kind], ...raw } : raw;
|
|
319
|
+
return parseContract(triggerConfigSchema, body);
|
|
309
320
|
}
|
|
310
321
|
/** Whether this trigger takes deliveries at a secret address rather than being checked. */
|
|
311
322
|
export function takesDeliveries(config) {
|