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.
Files changed (195) hide show
  1. package/CHANGELOG.md +102 -0
  2. package/README.md +7 -0
  3. package/dist/assignment-adapters.d.ts +12 -48
  4. package/dist/assignment-adapters.js +33 -43
  5. package/dist/assignment.d.ts +1 -0
  6. package/dist/assignment.js +2 -2
  7. package/dist/browser-workspace.d.ts +3 -3
  8. package/dist/build-review.d.ts +1 -1
  9. package/dist/build-review.js +2 -1
  10. package/dist/builder.js +4 -7
  11. package/dist/chat-acceptance.d.ts +2 -2
  12. package/dist/chat-actions.d.ts +18 -12
  13. package/dist/chat-actions.js +15 -65
  14. package/dist/chat-ask.js +5 -5
  15. package/dist/chat-decide.d.ts +14 -1
  16. package/dist/chat-decide.js +12 -6
  17. package/dist/chat-delivery-state.d.ts +12 -70
  18. package/dist/chat-delivery-state.js +17 -135
  19. package/dist/chat-delivery.js +58 -30
  20. package/dist/chat-flow.js +8 -6
  21. package/dist/chat-inbox.js +1 -1
  22. package/dist/coding-context.d.ts +2 -39
  23. package/dist/coding-context.js +2 -1
  24. package/dist/coding-handoff.d.ts +6 -25
  25. package/dist/coding-handoff.js +11 -5
  26. package/dist/coding-types.d.ts +2 -26
  27. package/dist/coding-workspace.d.ts +2 -0
  28. package/dist/coding-workspace.js +50 -14
  29. package/dist/contracts/acceptance-terms.d.ts +14 -0
  30. package/dist/contracts/acceptance-terms.js +16 -0
  31. package/dist/contracts/chat-actions.d.ts +328 -0
  32. package/dist/contracts/chat-actions.js +133 -0
  33. package/dist/contracts/chat-callback-rows.d.ts +59 -0
  34. package/dist/contracts/chat-callback-rows.js +46 -0
  35. package/dist/contracts/chat-content.d.ts +276 -0
  36. package/dist/contracts/chat-content.js +127 -0
  37. package/dist/contracts/chat-tables.d.ts +6 -0
  38. package/dist/contracts/chat-tables.js +136 -0
  39. package/dist/contracts/coding-context.d.ts +97 -0
  40. package/dist/contracts/coding-context.js +60 -0
  41. package/dist/contracts/coding-handoff.d.ts +44 -0
  42. package/dist/contracts/coding-handoff.js +47 -0
  43. package/dist/contracts/coding-workspace.d.ts +165 -0
  44. package/dist/contracts/coding-workspace.js +71 -0
  45. package/dist/contracts/contract.d.ts +40 -7
  46. package/dist/contracts/contract.js +155 -19
  47. package/dist/contracts/decision.d.ts +45 -0
  48. package/dist/contracts/decision.js +45 -0
  49. package/dist/contracts/discord-callback.d.ts +45 -0
  50. package/dist/contracts/discord-callback.js +34 -0
  51. package/dist/contracts/flow-card.d.ts +50 -0
  52. package/dist/contracts/flow-card.js +46 -0
  53. package/dist/contracts/flow-propose.d.ts +325 -0
  54. package/dist/contracts/flow-propose.js +41 -0
  55. package/dist/contracts/flow-send.d.ts +119 -0
  56. package/dist/contracts/flow-send.js +112 -0
  57. package/dist/contracts/flow.d.ts +19405 -0
  58. package/dist/contracts/flow.js +364 -0
  59. package/dist/contracts/gateway-tools.d.ts +374 -0
  60. package/dist/contracts/gateway-tools.js +94 -0
  61. package/dist/contracts/handoff.d.ts +91 -0
  62. package/dist/contracts/handoff.js +82 -0
  63. package/dist/contracts/lead-context.d.ts +165 -0
  64. package/dist/contracts/lead-context.js +73 -0
  65. package/dist/contracts/lead-tools.d.ts +1905 -0
  66. package/dist/contracts/lead-tools.js +265 -0
  67. package/dist/contracts/memory-pass.d.ts +158 -0
  68. package/dist/contracts/memory-pass.js +102 -0
  69. package/dist/contracts/project-knowledge.d.ts +126 -0
  70. package/dist/contracts/project-knowledge.js +93 -0
  71. package/dist/contracts/project-memory.d.ts +117 -0
  72. package/dist/contracts/project-memory.js +51 -0
  73. package/dist/contracts/project-skills.d.ts +121 -0
  74. package/dist/contracts/project-skills.js +76 -0
  75. package/dist/contracts/proof.d.ts +136 -0
  76. package/dist/contracts/proof.js +110 -0
  77. package/dist/contracts/review-findings.d.ts +46 -0
  78. package/dist/contracts/review-findings.js +48 -0
  79. package/dist/contracts/route.d.ts +484 -0
  80. package/dist/contracts/route.js +128 -0
  81. package/dist/contracts/scope.d.ts +105 -0
  82. package/dist/contracts/scope.js +114 -0
  83. package/dist/contracts/scout-report.d.ts +234 -0
  84. package/dist/contracts/scout-report.js +141 -0
  85. package/dist/contracts/slack-callback.d.ts +43 -0
  86. package/dist/contracts/slack-callback.js +35 -0
  87. package/dist/contracts/sort-answer.d.ts +83 -0
  88. package/dist/contracts/sort-answer.js +77 -0
  89. package/dist/contracts/stage-output.d.ts +127 -0
  90. package/dist/contracts/stage-output.js +166 -0
  91. package/dist/contracts/task-sizing.d.ts +24 -0
  92. package/dist/contracts/task-sizing.js +28 -0
  93. package/dist/contracts/teammate-turn.d.ts +43 -0
  94. package/dist/contracts/teammate-turn.js +51 -0
  95. package/dist/contracts/teams-callback.d.ts +14 -0
  96. package/dist/contracts/teams-callback.js +14 -0
  97. package/dist/contracts/telegram-callback.d.ts +120 -0
  98. package/dist/contracts/telegram-callback.js +97 -0
  99. package/dist/contracts/verification-receipt.d.ts +148 -0
  100. package/dist/contracts/verification-receipt.js +87 -0
  101. package/dist/decision.d.ts +11 -20
  102. package/dist/decision.js +86 -167
  103. package/dist/demo.js +28 -11
  104. package/dist/discord-chat.js +31 -20
  105. package/dist/evidence.d.ts +3 -40
  106. package/dist/evidence.js +5 -1
  107. package/dist/flow-engine.js +14 -17
  108. package/dist/flow-gallery.d.ts +3 -2
  109. package/dist/flow-items.d.ts +4 -13
  110. package/dist/flow-items.js +1 -15
  111. package/dist/flow-send.d.ts +6 -37
  112. package/dist/flow-send.js +18 -14
  113. package/dist/flow-share.d.ts +10 -32
  114. package/dist/flow-share.js +72 -84
  115. package/dist/flow-sort.d.ts +7 -26
  116. package/dist/flow-sort.js +25 -20
  117. package/dist/flow-starters.d.ts +2 -1
  118. package/dist/flow-steps.js +3 -2
  119. package/dist/flow-triggers.d.ts +15 -72
  120. package/dist/flow-triggers.js +94 -83
  121. package/dist/flows.d.ts +37 -220
  122. package/dist/flows.js +356 -387
  123. package/dist/invoke.js +4 -4
  124. package/dist/lead-context.d.ts +4 -2
  125. package/dist/lead-context.js +24 -17
  126. package/dist/mate-tools.d.ts +4 -0
  127. package/dist/mate-tools.js +111 -308
  128. package/dist/mcp-connect.d.ts +27 -4
  129. package/dist/mcp-connect.js +92 -17
  130. package/dist/mcp.js +84 -216
  131. package/dist/memory-pass.d.ts +17 -42
  132. package/dist/memory-pass.js +54 -26
  133. package/dist/operate.js +31 -2
  134. package/dist/phase-routing.d.ts +19 -72
  135. package/dist/phase-routing.js +34 -164
  136. package/dist/plan.d.ts +5 -5
  137. package/dist/plan.js +5 -25
  138. package/dist/process-recovery-eligibility.d.ts +2 -2
  139. package/dist/process-recovery-eligibility.js +13 -8
  140. package/dist/project-knowledge.d.ts +16 -35
  141. package/dist/project-knowledge.js +42 -21
  142. package/dist/project-memory.d.ts +2 -28
  143. package/dist/project-memory.js +28 -13
  144. package/dist/project-skills.d.ts +16 -38
  145. package/dist/project-skills.js +41 -32
  146. package/dist/project-tools.d.ts +1 -0
  147. package/dist/project-tools.js +4 -4
  148. package/dist/proof.d.ts +15 -91
  149. package/dist/proof.js +90 -249
  150. package/dist/provider.d.ts +2 -0
  151. package/dist/provider.js +4 -16
  152. package/dist/result-evidence-readers.d.ts +42 -0
  153. package/dist/result-evidence-readers.js +91 -0
  154. package/dist/result-shots.d.ts +1 -1
  155. package/dist/result-shots.js +2 -1
  156. package/dist/review-switch.d.ts +3 -8
  157. package/dist/reviewer.d.ts +8 -12
  158. package/dist/reviewer.js +11 -35
  159. package/dist/scope.d.ts +21 -43
  160. package/dist/scope.js +42 -128
  161. package/dist/scout-report.d.ts +9 -194
  162. package/dist/scout-report.js +113 -264
  163. package/dist/scout.js +15 -11
  164. package/dist/serve.js +17 -101
  165. package/dist/slack-chat.js +26 -20
  166. package/dist/slack-state.js +2 -1
  167. package/dist/store.d.ts +9 -32
  168. package/dist/store.js +18 -15
  169. package/dist/structured-output.js +2 -1
  170. package/dist/subscription-chat.d.ts +4 -0
  171. package/dist/subscription-chat.js +4 -1
  172. package/dist/surface.js +8 -1
  173. package/dist/task-review-brief.d.ts +69 -0
  174. package/dist/task-review-brief.js +179 -0
  175. package/dist/task-sizing.d.ts +3 -27
  176. package/dist/task-sizing.js +11 -26
  177. package/dist/task-text.d.ts +0 -5
  178. package/dist/task-text.js +1 -3
  179. package/dist/teammate-question.js +7 -5
  180. package/dist/teammate-work.js +9 -5
  181. package/dist/teammates.d.ts +13 -64
  182. package/dist/teammates.js +32 -33
  183. package/dist/teams-chat.js +20 -11
  184. package/dist/telegram-flow.js +4 -3
  185. package/dist/telegram-mate.d.ts +4 -14
  186. package/dist/telegram-mate.js +8 -7
  187. package/dist/telegram-team.d.ts +2 -19
  188. package/dist/telegram.d.ts +1 -1
  189. package/dist/telegram.js +52 -42
  190. package/dist/text-limits.d.ts +212 -1
  191. package/dist/text-limits.js +214 -1
  192. package/dist/tools-ui.d.ts +2 -1
  193. package/dist/tools-ui.js +7 -4
  194. package/dist/verification-evidence.js +24 -12
  195. package/package.json +1 -1
@@ -1,77 +1,12 @@
1
1
  import type { Runner } from "./backend.js";
2
2
  import { type FlowAct } from "./flow-engine.js";
3
- import type { FlowDefinition } from "./flows.js";
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 declare const FLOW_TRIGGER_KINDS: readonly ["button", "schedule", "github", "linear", "flow", "webhook", "email", "chat", "plane-review"];
7
- export type FlowTriggerKind = (typeof FLOW_TRIGGER_KINDS)[number];
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. Throws in plain words. */
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 {};
@@ -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 const FLOW_TRIGGER_KINDS = ["button", "schedule", "github", "linear", "flow", "webhook", "email", "chat", "plane-review"];
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
- function words(input, key, cap, required) {
58
- const value = input[key];
59
- if (value === undefined || value === null || (typeof value === "string" && value.trim() === "")) {
60
- if (required)
61
- throw new Error(`Say the trigger's ${key}.`);
62
- return null;
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
- if (typeof value !== "string")
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. Throws in plain words. */
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
- if (raw === null || typeof raw !== "object" || Array.isArray(raw))
122
- throw new Error("Choose what starts cards.");
123
- const input = raw;
124
- const kind = input["kind"];
125
- if (!FLOW_TRIGGER_KINDS.includes(kind))
126
- throw new Error("Choose what starts cards: a button, a schedule, GitHub, Linear, another flow, a webhook, an email inbox, a chat channel or a plane review.");
127
- const named = words(input, "zone", 60, false);
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
- throw new Error(`This flow has no zone called ${named}.`);
145
+ refuse("zone", `this flow has no zone called ${named}`);
131
146
  const zone = zoneStage?.id ?? null;
132
- const choice = (key, options, fallback) => {
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 = words(input, "label", 40, true);
143
- const asked = input["questions"];
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
- throw new Error("A button asks at most 6 questions.");
148
- for (const question of questions)
149
- if (question.length > 80 || scanForSecrets(question).length > 0)
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 said = words(input, "schedule", 80, true);
155
- const schedule = scheduleFromWords(said);
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 = words(input, "script", 40, false);
161
+ const script = said(input.script);
160
162
  if (script !== null) {
161
163
  if (context.store.flowScript(context.flow.repo, script) === null)
162
- throw new Error(`There's no script called ${script} in this project. Make it on the flow's Scripts panel first.`);
163
- const asked = input["secrets"];
164
- const secrets = asked === undefined || asked === null ? [] : Array.isArray(asked) ? asked : typeof asked === "string" ? asked.split(/[\s,]+/).filter(one => one !== "") : null;
165
- if (secrets === null || secrets.length > 10 || secrets.some(one => typeof one !== "string" || !/^[A-Z][A-Z0-9_]{0,39}$/.test(one)))
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: words(input, "title", 200, true), description: words(input, "description", 2000, false), zone };
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 = words(input, "repo", 140, false) ?? githubRepoOf(context.flow.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
- throw new Error("Name the GitHub repository as owner/name.");
175
- const watch = choice("watch", ["issues", "pulls", "checks"], "issues");
176
- const label = watch === "checks" ? null : words(input, "label", 50, false);
177
- const branch = watch === "checks" ? words(input, "branch", 100, false) ?? "main" : null;
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
- throw new Error("That branch name isn't valid.");
180
- return { kind: "github", repo, watch, label, branch, from: choice("from", ["team", "anyone"], "team"), delivery: choice("delivery", ["poll", "webhook"], "poll"), zone };
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 = words(input, "team", 12, false), state = words(input, "state", 40, false), label = words(input, "label", 50, false);
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
- throw new Error("A Linear team is its short key, like ENG.");
186
+ refuse("team", "a Linear team is its short key, like ENG");
186
187
  if (team === null && label === null)
187
- throw new Error("Name a Linear team or a label, so the trigger doesn't take every issue in the workspace.");
188
- return { kind: "linear", team: team?.toUpperCase() ?? null, state, label, delivery: choice("delivery", ["poll", "webhook"], "poll"), zone };
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 source = Number(input["flow"]);
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
- throw new Error("Choose another flow in your projects.");
195
+ return refuse("flow", "choose another flow in your projects");
196
196
  if (flow.id === context.flow.id)
197
- throw new Error("A flow can't start cards in itself; move them with a zone's next step instead.");
198
- const when = words(input, "when", 60, false);
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
- throw new Error(`${flow.name} has no zone called ${when ?? "Done"}.`);
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: words(input, "title", 120, false) ?? "Webhook", titleField: words(input, "titleField", 80, false), bodyField: words(input, "bodyField", 80, false), zone };
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 = words(input, "at", 5, false) ?? "07:30";
207
+ const at = said(input.at) ?? "07:30";
211
208
  const clock = /^(\d{1,2}):(\d{2})$/.exec(at);
212
- const zoneName = words(input, "timeZone", 60, false) ?? localTimeZone();
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
- throw new Error("Say the time it reviews the day as HH:MM, like 07:30, and a time zone like Europe/London.");
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 = words(input, "folder", 100, false) ?? "INBOX";
220
- const sender = words(input, "sender", 300, false);
216
+ const folder = said(input.folder) ?? "INBOX";
217
+ const sender = said(input.sender);
221
218
  if (sender !== null && sendersOf(sender).length === 0)
222
- throw new Error("Say whom mail comes from as addresses or domains, like priya@example.com or example.com.");
223
- return { kind: "email", folder: /^inbox$/i.test(folder) ? "INBOX" : folder, sender: sender === null ? null : sendersOf(sender).join(", "), subject: words(input, "subject", 100, false), zone };
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
- return JSON.parse(trigger.configJson);
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) {