toolroll 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (626) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +1113 -0
  3. package/THIRD_PARTY_NOTICES.md +449 -0
  4. package/dist/accent-colors.d.ts +48 -0
  5. package/dist/accent-colors.js +122 -0
  6. package/dist/action-ledger.d.ts +26 -0
  7. package/dist/action-ledger.js +75 -0
  8. package/dist/agent-fence.d.ts +42 -0
  9. package/dist/agent-fence.js +183 -0
  10. package/dist/agentconfig.d.ts +216 -0
  11. package/dist/agentconfig.js +454 -0
  12. package/dist/api-tokens.d.ts +16 -0
  13. package/dist/api-tokens.js +27 -0
  14. package/dist/approval-policy.d.ts +69 -0
  15. package/dist/approval-policy.js +137 -0
  16. package/dist/approval-rules-ui.d.ts +25 -0
  17. package/dist/approval-rules-ui.js +39 -0
  18. package/dist/assignment-adapters.d.ts +180 -0
  19. package/dist/assignment-adapters.js +239 -0
  20. package/dist/assignment-brief.d.ts +71 -0
  21. package/dist/assignment-brief.js +129 -0
  22. package/dist/assignment-delivery.d.ts +56 -0
  23. package/dist/assignment-delivery.js +160 -0
  24. package/dist/assignment-presentation.d.ts +26 -0
  25. package/dist/assignment-presentation.js +80 -0
  26. package/dist/assignment-status.d.ts +62 -0
  27. package/dist/assignment-status.js +152 -0
  28. package/dist/assignment-ui.d.ts +71 -0
  29. package/dist/assignment-ui.js +103 -0
  30. package/dist/assignment.d.ts +222 -0
  31. package/dist/assignment.js +399 -0
  32. package/dist/attest.d.ts +56 -0
  33. package/dist/attest.js +153 -0
  34. package/dist/backend.d.ts +97 -0
  35. package/dist/backend.js +166 -0
  36. package/dist/backup-ui.d.ts +22 -0
  37. package/dist/backup-ui.js +59 -0
  38. package/dist/backup.d.ts +84 -0
  39. package/dist/backup.js +421 -0
  40. package/dist/beads.d.ts +34 -0
  41. package/dist/beads.js +135 -0
  42. package/dist/bin.d.ts +2 -0
  43. package/dist/bin.js +22 -0
  44. package/dist/board.d.ts +150 -0
  45. package/dist/board.js +210 -0
  46. package/dist/boot-identity.d.ts +63 -0
  47. package/dist/boot-identity.js +99 -0
  48. package/dist/browser/THIRD_PARTY_NOTICES.txt +2295 -0
  49. package/dist/browser/workspace.css +4 -0
  50. package/dist/browser/workspace.js +225 -0
  51. package/dist/browser-crew.d.ts +11 -0
  52. package/dist/browser-crew.js +50 -0
  53. package/dist/browser-shell.d.ts +8 -0
  54. package/dist/browser-shell.js +60 -0
  55. package/dist/browser-workspace.d.ts +780 -0
  56. package/dist/browser-workspace.js +47 -0
  57. package/dist/budget-alerts.d.ts +19 -0
  58. package/dist/budget-alerts.js +46 -0
  59. package/dist/builder.d.ts +370 -0
  60. package/dist/builder.js +3262 -0
  61. package/dist/capscan.d.ts +35 -0
  62. package/dist/capscan.js +113 -0
  63. package/dist/chat-acceptance.d.ts +71 -0
  64. package/dist/chat-acceptance.js +120 -0
  65. package/dist/chat-actions.d.ts +265 -0
  66. package/dist/chat-actions.js +1273 -0
  67. package/dist/chat-channel.d.ts +109 -0
  68. package/dist/chat-channel.js +652 -0
  69. package/dist/chat-continuity.d.ts +32 -0
  70. package/dist/chat-continuity.js +317 -0
  71. package/dist/chat-controls.d.ts +113 -0
  72. package/dist/chat-controls.js +53 -0
  73. package/dist/chat-delivery-state.d.ts +159 -0
  74. package/dist/chat-delivery-state.js +309 -0
  75. package/dist/chat-delivery.d.ts +38 -0
  76. package/dist/chat-delivery.js +693 -0
  77. package/dist/chat-display.d.ts +1 -0
  78. package/dist/chat-display.js +18 -0
  79. package/dist/chat-evidence.d.ts +123 -0
  80. package/dist/chat-evidence.js +207 -0
  81. package/dist/chat-flow.d.ts +44 -0
  82. package/dist/chat-flow.js +215 -0
  83. package/dist/chat-inbox.d.ts +57 -0
  84. package/dist/chat-inbox.js +123 -0
  85. package/dist/chat-polish.d.ts +16 -0
  86. package/dist/chat-polish.js +135 -0
  87. package/dist/chat-review.d.ts +46 -0
  88. package/dist/chat-review.js +98 -0
  89. package/dist/chat-rooms.d.ts +123 -0
  90. package/dist/chat-rooms.js +216 -0
  91. package/dist/chat-task-actions.d.ts +44 -0
  92. package/dist/chat-task-actions.js +79 -0
  93. package/dist/check-progress.d.ts +51 -0
  94. package/dist/check-progress.js +290 -0
  95. package/dist/child-database.d.ts +12 -0
  96. package/dist/child-database.js +30 -0
  97. package/dist/claim.d.ts +688 -0
  98. package/dist/claim.js +1740 -0
  99. package/dist/cli.d.ts +137 -0
  100. package/dist/cli.js +1461 -0
  101. package/dist/codex-limits.d.ts +15 -0
  102. package/dist/codex-limits.js +145 -0
  103. package/dist/coding-context.d.ts +55 -0
  104. package/dist/coding-context.js +92 -0
  105. package/dist/coding-handoff.d.ts +57 -0
  106. package/dist/coding-handoff.js +277 -0
  107. package/dist/coding-provider.d.ts +72 -0
  108. package/dist/coding-provider.js +424 -0
  109. package/dist/coding-shipping-ui.d.ts +4 -0
  110. package/dist/coding-shipping-ui.js +12 -0
  111. package/dist/coding-types.d.ts +62 -0
  112. package/dist/coding-types.js +1 -0
  113. package/dist/coding-ui.d.ts +17 -0
  114. package/dist/coding-ui.js +364 -0
  115. package/dist/coding-update.d.ts +15 -0
  116. package/dist/coding-update.js +149 -0
  117. package/dist/coding-workspace.d.ts +124 -0
  118. package/dist/coding-workspace.js +768 -0
  119. package/dist/container-state.d.ts +5 -0
  120. package/dist/container-state.js +62 -0
  121. package/dist/containment.d.ts +197 -0
  122. package/dist/containment.js +559 -0
  123. package/dist/contest.d.ts +318 -0
  124. package/dist/contest.js +753 -0
  125. package/dist/control-setup.d.ts +35 -0
  126. package/dist/control-setup.js +40 -0
  127. package/dist/control-ui.d.ts +29 -0
  128. package/dist/control-ui.js +53 -0
  129. package/dist/controller-service.d.ts +1 -0
  130. package/dist/controller-service.js +19 -0
  131. package/dist/controller-supervisor.d.ts +30 -0
  132. package/dist/controller-supervisor.js +104 -0
  133. package/dist/converse.d.ts +304 -0
  134. package/dist/converse.js +849 -0
  135. package/dist/coordinator-proposals.d.ts +29 -0
  136. package/dist/coordinator-proposals.js +137 -0
  137. package/dist/coordinator.d.ts +186 -0
  138. package/dist/coordinator.js +447 -0
  139. package/dist/credentials-ui.d.ts +25 -0
  140. package/dist/credentials-ui.js +48 -0
  141. package/dist/daemon.d.ts +209 -0
  142. package/dist/daemon.js +604 -0
  143. package/dist/decision.d.ts +120 -0
  144. package/dist/decision.js +388 -0
  145. package/dist/demo.d.ts +55 -0
  146. package/dist/demo.js +992 -0
  147. package/dist/desktop-access.d.ts +28 -0
  148. package/dist/desktop-access.js +88 -0
  149. package/dist/desktop-bundle.d.ts +16 -0
  150. package/dist/desktop-bundle.js +74 -0
  151. package/dist/desktop-host.d.ts +54 -0
  152. package/dist/desktop-host.js +508 -0
  153. package/dist/desktop-update-gate.d.ts +12 -0
  154. package/dist/desktop-update-gate.js +91 -0
  155. package/dist/desktop-update-recovery.d.ts +14 -0
  156. package/dist/desktop-update-recovery.js +243 -0
  157. package/dist/desktop-update.d.ts +110 -0
  158. package/dist/desktop-update.js +740 -0
  159. package/dist/discord-api.d.ts +20 -0
  160. package/dist/discord-api.js +138 -0
  161. package/dist/discord-chat.d.ts +16 -0
  162. package/dist/discord-chat.js +381 -0
  163. package/dist/discord-settings.d.ts +7 -0
  164. package/dist/discord-settings.js +75 -0
  165. package/dist/discord.d.ts +10 -0
  166. package/dist/discord.js +169 -0
  167. package/dist/discover.d.ts +75 -0
  168. package/dist/discover.js +150 -0
  169. package/dist/dispatch.d.ts +100 -0
  170. package/dist/dispatch.js +311 -0
  171. package/dist/dispose.d.ts +161 -0
  172. package/dist/dispose.js +803 -0
  173. package/dist/email-settings.d.ts +34 -0
  174. package/dist/email-settings.js +50 -0
  175. package/dist/envelope.d.ts +47 -0
  176. package/dist/envelope.js +105 -0
  177. package/dist/evidence-pack.d.ts +185 -0
  178. package/dist/evidence-pack.js +258 -0
  179. package/dist/evidence.d.ts +345 -0
  180. package/dist/evidence.js +782 -0
  181. package/dist/exec.d.ts +285 -0
  182. package/dist/exec.js +1841 -0
  183. package/dist/exhaustion.d.ts +85 -0
  184. package/dist/exhaustion.js +141 -0
  185. package/dist/export-ui.d.ts +4 -0
  186. package/dist/export-ui.js +17 -0
  187. package/dist/export.d.ts +48 -0
  188. package/dist/export.js +305 -0
  189. package/dist/flow-actions.d.ts +77 -0
  190. package/dist/flow-actions.js +203 -0
  191. package/dist/flow-code.d.ts +50 -0
  192. package/dist/flow-code.js +159 -0
  193. package/dist/flow-draft.d.ts +38 -0
  194. package/dist/flow-draft.js +80 -0
  195. package/dist/flow-engine.d.ts +66 -0
  196. package/dist/flow-engine.js +416 -0
  197. package/dist/flow-insights.d.ts +95 -0
  198. package/dist/flow-insights.js +149 -0
  199. package/dist/flow-live.d.ts +24 -0
  200. package/dist/flow-live.js +150 -0
  201. package/dist/flow-people.d.ts +24 -0
  202. package/dist/flow-people.js +91 -0
  203. package/dist/flow-replies.d.ts +48 -0
  204. package/dist/flow-replies.js +113 -0
  205. package/dist/flow-scripts.d.ts +38 -0
  206. package/dist/flow-scripts.js +73 -0
  207. package/dist/flow-secrets.d.ts +13 -0
  208. package/dist/flow-secrets.js +45 -0
  209. package/dist/flow-sort.d.ts +82 -0
  210. package/dist/flow-sort.js +153 -0
  211. package/dist/flow-steps.d.ts +40 -0
  212. package/dist/flow-steps.js +408 -0
  213. package/dist/flow-triggers.d.ts +244 -0
  214. package/dist/flow-triggers.js +959 -0
  215. package/dist/flows-ui.d.ts +20 -0
  216. package/dist/flows-ui.js +175 -0
  217. package/dist/flows.d.ts +229 -0
  218. package/dist/flows.js +968 -0
  219. package/dist/fonts.d.ts +19 -0
  220. package/dist/fonts.js +19 -0
  221. package/dist/gaps.d.ts +35 -0
  222. package/dist/gaps.js +101 -0
  223. package/dist/gate-failure.d.ts +40 -0
  224. package/dist/gate-failure.js +66 -0
  225. package/dist/git.d.ts +54 -0
  226. package/dist/git.js +94 -0
  227. package/dist/google-mail.d.ts +58 -0
  228. package/dist/google-mail.js +151 -0
  229. package/dist/grant.d.ts +136 -0
  230. package/dist/grant.js +238 -0
  231. package/dist/graph.d.ts +164 -0
  232. package/dist/graph.js +383 -0
  233. package/dist/guides.d.ts +22 -0
  234. package/dist/guides.js +363 -0
  235. package/dist/held.d.ts +150 -0
  236. package/dist/held.js +799 -0
  237. package/dist/invoke.d.ts +112 -0
  238. package/dist/invoke.js +780 -0
  239. package/dist/issues.d.ts +41 -0
  240. package/dist/issues.js +131 -0
  241. package/dist/job-object-helper.ps1 +252 -0
  242. package/dist/jsonl-discriminants.d.ts +11 -0
  243. package/dist/jsonl-discriminants.js +93 -0
  244. package/dist/keys.d.ts +104 -0
  245. package/dist/keys.js +229 -0
  246. package/dist/kits-ui.d.ts +17 -0
  247. package/dist/kits-ui.js +46 -0
  248. package/dist/kits.d.ts +86 -0
  249. package/dist/kits.js +215 -0
  250. package/dist/knowledge-cli.d.ts +101 -0
  251. package/dist/knowledge-cli.js +92 -0
  252. package/dist/knowledge-ui.d.ts +24 -0
  253. package/dist/knowledge-ui.js +40 -0
  254. package/dist/lead-context.d.ts +8 -0
  255. package/dist/lead-context.js +56 -0
  256. package/dist/lead-follow.d.ts +22 -0
  257. package/dist/lead-follow.js +167 -0
  258. package/dist/lead-status.d.ts +75 -0
  259. package/dist/lead-status.js +285 -0
  260. package/dist/ledger-chain.d.ts +46 -0
  261. package/dist/ledger-chain.js +187 -0
  262. package/dist/ledger-csv.d.ts +4 -0
  263. package/dist/ledger-csv.js +11 -0
  264. package/dist/ledger-view.d.ts +24 -0
  265. package/dist/ledger-view.js +58 -0
  266. package/dist/limits-ui.d.ts +26 -0
  267. package/dist/limits-ui.js +67 -0
  268. package/dist/link.d.ts +72 -0
  269. package/dist/link.js +217 -0
  270. package/dist/live.d.ts +96 -0
  271. package/dist/live.js +366 -0
  272. package/dist/liveness.d.ts +30 -0
  273. package/dist/liveness.js +42 -0
  274. package/dist/log.d.ts +7 -0
  275. package/dist/log.js +25 -0
  276. package/dist/mailbox.d.ts +57 -0
  277. package/dist/mailbox.js +150 -0
  278. package/dist/maintenance.d.ts +11 -0
  279. package/dist/maintenance.js +35 -0
  280. package/dist/mate-cli.d.ts +52 -0
  281. package/dist/mate-cli.js +345 -0
  282. package/dist/mate-contract.d.ts +10 -0
  283. package/dist/mate-contract.js +30 -0
  284. package/dist/mate-doors.d.ts +69 -0
  285. package/dist/mate-doors.js +548 -0
  286. package/dist/mate-progress.d.ts +29 -0
  287. package/dist/mate-progress.js +121 -0
  288. package/dist/mate-tools.d.ts +94 -0
  289. package/dist/mate-tools.js +1878 -0
  290. package/dist/mate.d.ts +92 -0
  291. package/dist/mate.js +435 -0
  292. package/dist/mcp-connect.d.ts +103 -0
  293. package/dist/mcp-connect.js +252 -0
  294. package/dist/mcp.d.ts +27 -0
  295. package/dist/mcp.js +651 -0
  296. package/dist/memory-cli.d.ts +466 -0
  297. package/dist/memory-cli.js +111 -0
  298. package/dist/memory-pass.d.ts +160 -0
  299. package/dist/memory-pass.js +409 -0
  300. package/dist/metrics.d.ts +3 -0
  301. package/dist/metrics.js +56 -0
  302. package/dist/mobile-viewport.d.ts +3 -0
  303. package/dist/mobile-viewport.js +37 -0
  304. package/dist/model-catalog.d.ts +118 -0
  305. package/dist/model-catalog.js +376 -0
  306. package/dist/models-cli.d.ts +102 -0
  307. package/dist/models-cli.js +66 -0
  308. package/dist/models-ui.d.ts +34 -0
  309. package/dist/models-ui.js +68 -0
  310. package/dist/modes.d.ts +80 -0
  311. package/dist/modes.js +182 -0
  312. package/dist/monitoring-settings.d.ts +44 -0
  313. package/dist/monitoring-settings.js +173 -0
  314. package/dist/monitoring-ui.d.ts +20 -0
  315. package/dist/monitoring-ui.js +51 -0
  316. package/dist/monitoring.d.ts +34 -0
  317. package/dist/monitoring.js +221 -0
  318. package/dist/names.d.ts +67 -0
  319. package/dist/names.js +102 -0
  320. package/dist/observations.d.ts +40 -0
  321. package/dist/observations.js +211 -0
  322. package/dist/oidc.d.ts +71 -0
  323. package/dist/oidc.js +182 -0
  324. package/dist/onboard.d.ts +108 -0
  325. package/dist/onboard.js +325 -0
  326. package/dist/openrouter-models.d.ts +30 -0
  327. package/dist/openrouter-models.js +120 -0
  328. package/dist/operate.d.ts +118 -0
  329. package/dist/operate.js +11321 -0
  330. package/dist/peek-cli.d.ts +97 -0
  331. package/dist/peek-cli.js +215 -0
  332. package/dist/peek.d.ts +134 -0
  333. package/dist/peek.js +578 -0
  334. package/dist/phase-routing.d.ts +307 -0
  335. package/dist/phase-routing.js +658 -0
  336. package/dist/plan-auto.d.ts +14 -0
  337. package/dist/plan-auto.js +92 -0
  338. package/dist/plan.d.ts +186 -0
  339. package/dist/plan.js +401 -0
  340. package/dist/planner-source.d.ts +251 -0
  341. package/dist/planner-source.js +460 -0
  342. package/dist/planner.d.ts +75 -0
  343. package/dist/planner.js +992 -0
  344. package/dist/policy-ui.d.ts +20 -0
  345. package/dist/policy-ui.js +41 -0
  346. package/dist/policy.d.ts +111 -0
  347. package/dist/policy.js +231 -0
  348. package/dist/prepared-evidence.d.ts +16 -0
  349. package/dist/prepared-evidence.js +96 -0
  350. package/dist/pricing.d.ts +45 -0
  351. package/dist/pricing.js +77 -0
  352. package/dist/principal.d.ts +42 -0
  353. package/dist/principal.js +82 -0
  354. package/dist/probe.d.ts +46 -0
  355. package/dist/probe.js +79 -0
  356. package/dist/process-custody.d.ts +17 -0
  357. package/dist/process-custody.js +91 -0
  358. package/dist/process-liveness.d.ts +10 -0
  359. package/dist/process-liveness.js +81 -0
  360. package/dist/process-recovery-anchor.d.ts +75 -0
  361. package/dist/process-recovery-anchor.js +188 -0
  362. package/dist/process-recovery-coalition.d.ts +25 -0
  363. package/dist/process-recovery-coalition.js +105 -0
  364. package/dist/process-recovery-eligibility.d.ts +178 -0
  365. package/dist/process-recovery-eligibility.js +170 -0
  366. package/dist/process-recovery-native.d.ts +172 -0
  367. package/dist/process-recovery-native.js +592 -0
  368. package/dist/process-recovery-provenance.d.ts +163 -0
  369. package/dist/process-recovery-provenance.js +292 -0
  370. package/dist/process-recovery-services.d.ts +56 -0
  371. package/dist/process-recovery-services.js +191 -0
  372. package/dist/process-recovery-settlement.d.ts +32 -0
  373. package/dist/process-recovery-settlement.js +132 -0
  374. package/dist/process-recovery.d.ts +41 -0
  375. package/dist/process-recovery.js +91 -0
  376. package/dist/process-tree.d.ts +41 -0
  377. package/dist/process-tree.js +264 -0
  378. package/dist/project-access.d.ts +14 -0
  379. package/dist/project-access.js +34 -0
  380. package/dist/project-cli.d.ts +18 -0
  381. package/dist/project-cli.js +104 -0
  382. package/dist/project-delete-ui.d.ts +22 -0
  383. package/dist/project-delete-ui.js +48 -0
  384. package/dist/project-delete.d.ts +55 -0
  385. package/dist/project-delete.js +413 -0
  386. package/dist/project-knowledge.d.ts +111 -0
  387. package/dist/project-knowledge.js +241 -0
  388. package/dist/project-learning.d.ts +100 -0
  389. package/dist/project-learning.js +438 -0
  390. package/dist/project-memory.d.ts +81 -0
  391. package/dist/project-memory.js +264 -0
  392. package/dist/project-skills.d.ts +152 -0
  393. package/dist/project-skills.js +660 -0
  394. package/dist/project-tools.d.ts +219 -0
  395. package/dist/project-tools.js +796 -0
  396. package/dist/project.d.ts +61 -0
  397. package/dist/project.js +125 -0
  398. package/dist/prompt.d.ts +22 -0
  399. package/dist/prompt.js +72 -0
  400. package/dist/proof.d.ts +513 -0
  401. package/dist/proof.js +1140 -0
  402. package/dist/proposal.d.ts +82 -0
  403. package/dist/proposal.js +210 -0
  404. package/dist/provider-connection.d.ts +22 -0
  405. package/dist/provider-connection.js +98 -0
  406. package/dist/provider-limits.d.ts +35 -0
  407. package/dist/provider-limits.js +95 -0
  408. package/dist/provider.d.ts +279 -0
  409. package/dist/provider.js +935 -0
  410. package/dist/publish.d.ts +107 -0
  411. package/dist/publish.js +650 -0
  412. package/dist/pulls.d.ts +118 -0
  413. package/dist/pulls.js +240 -0
  414. package/dist/push.d.ts +95 -0
  415. package/dist/push.js +353 -0
  416. package/dist/quality.d.ts +8 -0
  417. package/dist/quality.js +6 -0
  418. package/dist/recipe-ui.d.ts +18 -0
  419. package/dist/recipe-ui.js +152 -0
  420. package/dist/recipes.d.ts +81 -0
  421. package/dist/recipes.js +332 -0
  422. package/dist/remote.d.ts +67 -0
  423. package/dist/remote.js +122 -0
  424. package/dist/render.d.ts +64 -0
  425. package/dist/render.js +587 -0
  426. package/dist/report-summary.d.ts +13 -0
  427. package/dist/report-summary.js +15 -0
  428. package/dist/repos.d.ts +65 -0
  429. package/dist/repos.js +277 -0
  430. package/dist/repository-context-ui.d.ts +2 -0
  431. package/dist/repository-context-ui.js +10 -0
  432. package/dist/repository-context.d.ts +67 -0
  433. package/dist/repository-context.js +376 -0
  434. package/dist/restart-certification.d.ts +134 -0
  435. package/dist/restart-certification.js +226 -0
  436. package/dist/result-actions.d.ts +28 -0
  437. package/dist/result-actions.js +270 -0
  438. package/dist/result-completion.d.ts +18 -0
  439. package/dist/result-completion.js +66 -0
  440. package/dist/result-review.d.ts +213 -0
  441. package/dist/result-review.js +430 -0
  442. package/dist/retention-ui.d.ts +12 -0
  443. package/dist/retention-ui.js +39 -0
  444. package/dist/retention.d.ts +72 -0
  445. package/dist/retention.js +288 -0
  446. package/dist/review-context.d.ts +243 -0
  447. package/dist/review-context.js +1043 -0
  448. package/dist/review-evidence.d.ts +33 -0
  449. package/dist/review-evidence.js +56 -0
  450. package/dist/reviewer.d.ts +97 -0
  451. package/dist/reviewer.js +202 -0
  452. package/dist/routine.d.ts +228 -0
  453. package/dist/routine.js +764 -0
  454. package/dist/runner.d.ts +266 -0
  455. package/dist/runner.js +399 -0
  456. package/dist/scan.d.ts +32 -0
  457. package/dist/scan.js +92 -0
  458. package/dist/scope.d.ts +695 -0
  459. package/dist/scope.js +1176 -0
  460. package/dist/scout-report.d.ts +42 -0
  461. package/dist/scout-report.js +108 -0
  462. package/dist/scout.d.ts +69 -0
  463. package/dist/scout.js +351 -0
  464. package/dist/serve.d.ts +313 -0
  465. package/dist/serve.js +21776 -0
  466. package/dist/session-brief.d.ts +8 -0
  467. package/dist/session-brief.js +24 -0
  468. package/dist/session-cli.d.ts +13 -0
  469. package/dist/session-cli.js +284 -0
  470. package/dist/session-contract.d.ts +183 -0
  471. package/dist/session-contract.js +122 -0
  472. package/dist/session-http.d.ts +19 -0
  473. package/dist/session-http.js +154 -0
  474. package/dist/session-server.d.ts +12 -0
  475. package/dist/session-server.js +37 -0
  476. package/dist/session-service.d.ts +22 -0
  477. package/dist/session-service.js +157 -0
  478. package/dist/setup-guide.d.ts +39 -0
  479. package/dist/setup-guide.js +93 -0
  480. package/dist/sign-in-guard.d.ts +45 -0
  481. package/dist/sign-in-guard.js +74 -0
  482. package/dist/skills-ui.d.ts +11 -0
  483. package/dist/skills-ui.js +58 -0
  484. package/dist/skills.d.ts +97 -0
  485. package/dist/skills.js +276 -0
  486. package/dist/slack-api.d.ts +55 -0
  487. package/dist/slack-api.js +229 -0
  488. package/dist/slack-chat.d.ts +28 -0
  489. package/dist/slack-chat.js +413 -0
  490. package/dist/slack-settings.d.ts +7 -0
  491. package/dist/slack-settings.js +75 -0
  492. package/dist/slack-state.d.ts +13 -0
  493. package/dist/slack-state.js +9 -0
  494. package/dist/slack.d.ts +13 -0
  495. package/dist/slack.js +167 -0
  496. package/dist/spend-ui.d.ts +28 -0
  497. package/dist/spend-ui.js +95 -0
  498. package/dist/spend.d.ts +142 -0
  499. package/dist/spend.js +313 -0
  500. package/dist/sqlite-runtime.d.ts +3 -0
  501. package/dist/sqlite-runtime.js +6 -0
  502. package/dist/sso-settings.d.ts +33 -0
  503. package/dist/sso-settings.js +103 -0
  504. package/dist/sso-ui.d.ts +22 -0
  505. package/dist/sso-ui.js +39 -0
  506. package/dist/storage.d.ts +15 -0
  507. package/dist/storage.js +84 -0
  508. package/dist/store.d.ts +7754 -0
  509. package/dist/store.js +23279 -0
  510. package/dist/structured-output.d.ts +58 -0
  511. package/dist/structured-output.js +103 -0
  512. package/dist/style-asset.d.ts +7 -0
  513. package/dist/style-asset.js +41 -0
  514. package/dist/subscription-chat.d.ts +46 -0
  515. package/dist/subscription-chat.js +275 -0
  516. package/dist/summary.d.ts +33 -0
  517. package/dist/summary.js +71 -0
  518. package/dist/supervisor.mjs +297 -0
  519. package/dist/surface.d.ts +63 -0
  520. package/dist/surface.js +352 -0
  521. package/dist/sync.d.ts +94 -0
  522. package/dist/sync.js +279 -0
  523. package/dist/task-composer.d.ts +13 -0
  524. package/dist/task-composer.js +52 -0
  525. package/dist/task-control.d.ts +165 -0
  526. package/dist/task-control.js +214 -0
  527. package/dist/task-outcome-cli.d.ts +10 -0
  528. package/dist/task-outcome-cli.js +92 -0
  529. package/dist/task-text.d.ts +30 -0
  530. package/dist/task-text.js +41 -0
  531. package/dist/team-cli.d.ts +15 -0
  532. package/dist/team-cli.js +618 -0
  533. package/dist/team-contract.d.ts +92 -0
  534. package/dist/team-contract.js +1 -0
  535. package/dist/team-http.d.ts +17 -0
  536. package/dist/team-http.js +144 -0
  537. package/dist/team-leads.d.ts +81 -0
  538. package/dist/team-leads.js +565 -0
  539. package/dist/team-runtime.d.ts +27 -0
  540. package/dist/team-runtime.js +264 -0
  541. package/dist/team-ui.d.ts +3 -0
  542. package/dist/team-ui.js +9 -0
  543. package/dist/team-updates.d.ts +8 -0
  544. package/dist/team-updates.js +57 -0
  545. package/dist/teammate-admin.d.ts +53 -0
  546. package/dist/teammate-admin.js +111 -0
  547. package/dist/teammate-desk.d.ts +60 -0
  548. package/dist/teammate-desk.js +154 -0
  549. package/dist/teammate-memory.d.ts +64 -0
  550. package/dist/teammate-memory.js +196 -0
  551. package/dist/teammate-question.d.ts +59 -0
  552. package/dist/teammate-question.js +175 -0
  553. package/dist/teammate-tools.d.ts +108 -0
  554. package/dist/teammate-tools.js +271 -0
  555. package/dist/teammate-week.d.ts +68 -0
  556. package/dist/teammate-week.js +132 -0
  557. package/dist/teammate-work.d.ts +76 -0
  558. package/dist/teammate-work.js +280 -0
  559. package/dist/teammates-ui.d.ts +20 -0
  560. package/dist/teammates-ui.js +166 -0
  561. package/dist/teammates.d.ts +180 -0
  562. package/dist/teammates.js +253 -0
  563. package/dist/teams-api.d.ts +33 -0
  564. package/dist/teams-api.js +204 -0
  565. package/dist/teams-chat.d.ts +26 -0
  566. package/dist/teams-chat.js +215 -0
  567. package/dist/teams-settings.d.ts +12 -0
  568. package/dist/teams-settings.js +57 -0
  569. package/dist/teams.d.ts +21 -0
  570. package/dist/teams.js +124 -0
  571. package/dist/telegram-flow.d.ts +49 -0
  572. package/dist/telegram-flow.js +110 -0
  573. package/dist/telegram-mate.d.ts +137 -0
  574. package/dist/telegram-mate.js +707 -0
  575. package/dist/telegram-progress.d.ts +14 -0
  576. package/dist/telegram-progress.js +129 -0
  577. package/dist/telegram-settings.d.ts +9 -0
  578. package/dist/telegram-settings.js +36 -0
  579. package/dist/telegram-status.d.ts +75 -0
  580. package/dist/telegram-status.js +265 -0
  581. package/dist/telegram-team.d.ts +85 -0
  582. package/dist/telegram-team.js +359 -0
  583. package/dist/telegram.d.ts +230 -0
  584. package/dist/telegram.js +1931 -0
  585. package/dist/templates.d.ts +65 -0
  586. package/dist/templates.js +118 -0
  587. package/dist/tool-launcher.d.ts +1 -0
  588. package/dist/tool-launcher.js +101 -0
  589. package/dist/tools-ui.d.ts +30 -0
  590. package/dist/tools-ui.js +40 -0
  591. package/dist/transitions-recipes.d.ts +1 -0
  592. package/dist/transitions-recipes.js +203 -0
  593. package/dist/tree-proof.d.ts +34 -0
  594. package/dist/tree-proof.js +58 -0
  595. package/dist/verification-evidence.d.ts +44 -0
  596. package/dist/verification-evidence.js +238 -0
  597. package/dist/version.d.ts +2 -0
  598. package/dist/version.js +10 -0
  599. package/dist/webhooks.d.ts +91 -0
  600. package/dist/webhooks.js +283 -0
  601. package/dist/work-index.d.ts +68 -0
  602. package/dist/work-index.js +384 -0
  603. package/dist/work-summary.d.ts +57 -0
  604. package/dist/work-summary.js +116 -0
  605. package/dist/workspace-motion.d.ts +4 -0
  606. package/dist/workspace-motion.js +133 -0
  607. package/dist/workspace-revision.d.ts +19 -0
  608. package/dist/workspace-revision.js +92 -0
  609. package/dist/workspace-ui.d.ts +204 -0
  610. package/dist/workspace-ui.js +411 -0
  611. package/dist/worktree-notices.d.ts +13 -0
  612. package/dist/worktree-notices.js +17 -0
  613. package/dist/worktree.d.ts +249 -0
  614. package/dist/worktree.js +720 -0
  615. package/package.json +122 -0
  616. package/scripts/canary-assertions.mjs +42 -0
  617. package/scripts/crash-canary.mjs +253 -0
  618. package/scripts/fixtures/crash-process.mjs +74 -0
  619. package/scripts/fixtures/pilot-scenarios.mjs +85 -0
  620. package/scripts/fixtures/restart-service.mjs +27 -0
  621. package/scripts/launchd-certification.mjs +143 -0
  622. package/scripts/pilot.mjs +121 -0
  623. package/scripts/proof-preflight.mjs +140 -0
  624. package/scripts/provider-canary.mjs +379 -0
  625. package/scripts/recovery-canary.mjs +135 -0
  626. package/scripts/restart-certification.mjs +98 -0
@@ -0,0 +1,959 @@
1
+ /**
2
+ * Flow triggers (v82): what starts cards in a flow without someone adding
3
+ * them by hand.
4
+ *
5
+ * - button — a named button with questions; pressing it makes a card from the answers.
6
+ * - schedule — a card on a schedule (the routines' schedule rules: every N minutes, daily, weekly);
7
+ * v90: or a project script run on the schedule, each item it prints a card.
8
+ * - github — new issues (optionally with a label), new pull requests, or failed checks on a branch.
9
+ * - linear — Linear issues in a team, moving into a state, and/or with a label.
10
+ * - flow — another flow's cards reaching one of its zones.
11
+ * - webhook — anything that can post JSON to the trigger's secret address.
12
+ * - email — mail arriving in a folder of the account set up in Settings → Email (v89),
13
+ * read-only over IMAP; optionally only from some senders or with words in the subject.
14
+ * - chat — messages in a Slack, Discord or Teams channel (v89), connected from the
15
+ * channel itself (chat-inbox.ts), never from here.
16
+ *
17
+ * GitHub and Linear are checked by the worker's pass — `gh` and one HTTPS
18
+ * request, never a model — or pushed to a secret webhook address when the
19
+ * console has a public one. Either way one outside thing makes at most one
20
+ * card; outside text is checked for keys before it becomes a card; GitHub
21
+ * text from outside the team is left out unless the trigger says anyone;
22
+ * and a card's work is an ordinary task under the usual approvals.
23
+ */
24
+ import { createHash, createHmac, randomBytes, randomUUID, timingSafeEqual } from "node:crypto";
25
+ import { chmodSync, existsSync, mkdirSync, readFileSync, rmSync, statSync, writeFileSync } from "node:fs";
26
+ import { join } from "node:path";
27
+ import { scanForSecrets } from "./evidence.js";
28
+ import { flowCardText, flowDefinitionOf } from "./flow-engine.js";
29
+ import { describeSchedule, firstFireAt, nextFireAt, parseSchedule, WEEKDAYS } from "./routine.js";
30
+ import { takeReply } from "./flow-replies.js";
31
+ import { mailboxAccess, mailCursorOf, mailCursorText, readThroughImap } from "./mailbox.js";
32
+ import { readFlowSecrets } from "./flow-secrets.js";
33
+ import { tmpdir } from "node:os";
34
+ import { envValue } from "./names.js";
35
+ export const FLOW_TRIGGER_KINDS = ["button", "schedule", "github", "linear", "flow", "webhook", "email", "chat"];
36
+ export const CHAT_APP_NAMES = { slack: "Slack", discord: "Discord", teams: "Teams", telegram: "Telegram" };
37
+ /** What each kind is called on the canvas. */
38
+ export const FLOW_TRIGGER_WORDS = {
39
+ button: "Button", schedule: "Schedule", github: "GitHub", linear: "Linear", flow: "Another flow", webhook: "Webhook", email: "Email inbox", chat: "Chat channel",
40
+ };
41
+ /** How often the worker checks GitHub and Linear, and how it backs off when they don't answer. */
42
+ export const POLL_EVERY_MS = 2 * 60_000;
43
+ const BACKOFF_MS = [5 * 60_000, 15 * 60_000, 60 * 60_000];
44
+ /** At most this many cards from one check or one delivery; the rest wait for the next. */
45
+ export const CARDS_PER_CHECK = 10;
46
+ const HOOK_BYTES = 1_000_000;
47
+ /** Authors GitHub says have write access to the repository. */
48
+ const TEAM = new Set(["OWNER", "MEMBER", "COLLABORATOR"]);
49
+ // ------------------------------------------------------------ the terms
50
+ function words(input, key, cap, required) {
51
+ const value = input[key];
52
+ if (value === undefined || value === null || (typeof value === "string" && value.trim() === "")) {
53
+ if (required)
54
+ throw new Error(`Say the trigger's ${key}.`);
55
+ return null;
56
+ }
57
+ if (typeof value !== "string")
58
+ throw new Error(`The trigger's ${key} must be plain text.`);
59
+ const trimmed = value.trim();
60
+ if (trimmed.length > cap)
61
+ throw new Error(`The trigger's ${key} is up to ${cap} characters.`);
62
+ if (/[\u0000-\u0008\u000b\u000c\u000e-\u001f\u007f\u200b-\u200f\u202a-\u202e\u2066-\u2069]/.test(trimmed))
63
+ throw new Error(`The trigger's ${key} can't contain hidden characters.`);
64
+ if (scanForSecrets(trimmed).length > 0)
65
+ throw new Error("That looks like a key or password. Keys never go in a trigger's settings.");
66
+ return trimmed;
67
+ }
68
+ /** A schedule said in words ("every 2 hours", "daily 09:00 Europe/London", "monday 09:00") or in the routines' own form. */
69
+ export function scheduleFromWords(text) {
70
+ const raw = text.trim().replace(/\s+/g, " ");
71
+ if (parseSchedule(raw) !== null)
72
+ return raw;
73
+ const zone = (value) => value === undefined || /^utc$/i.test(value) ? "" : `@${value}`;
74
+ const pad = (hour) => hour.padStart(2, "0");
75
+ let match = /^every (\d{1,5}) ?(minutes?|mins?|m|hours?|hrs?|h|days?|d)$/i.exec(raw);
76
+ if (match !== null) {
77
+ const n = Number(match[1]), unit = match[2].toLowerCase()[0];
78
+ const text = `every:${unit === "m" ? n : unit === "h" ? n * 60 : n * 1440}`;
79
+ return parseSchedule(text) === null ? null : text;
80
+ }
81
+ match = /^(?:every day|daily)(?: at)? (\d{1,2}):(\d{2})(?: (\S+))?$/i.exec(raw);
82
+ if (match !== null) {
83
+ const text = `daily:${pad(match[1])}:${match[2]}${zone(match[3])}`;
84
+ return parseSchedule(text) === null ? null : text;
85
+ }
86
+ match = /^(?:every )?(?:weekdays?|weekday mornings?)(?: at)? (\d{1,2}):(\d{2})(?: (\S+))?$/i.exec(raw);
87
+ if (match !== null) {
88
+ const text = `weekdays:${pad(match[1])}:${match[2]}${zone(match[3])}`;
89
+ return parseSchedule(text) === null ? null : text;
90
+ }
91
+ match = /^(?:every |weekly on |weekly |on )?(sunday|monday|tuesday|wednesday|thursday|friday|saturday)s?(?: at)? (\d{1,2}):(\d{2})(?: (\S+))?$/i.exec(raw);
92
+ if (match !== null) {
93
+ const day = WEEKDAYS.findIndex(one => one.toLowerCase() === match[1].toLowerCase());
94
+ const text = `weekly:${day}:${pad(match[2])}:${match[3]}${zone(match[4])}`;
95
+ return parseSchedule(text) === null ? null : text;
96
+ }
97
+ return null;
98
+ }
99
+ /** A GitHub repository as owner/name, read from a project's origin remote when it is on GitHub. */
100
+ export function githubRepoOf(path) {
101
+ try {
102
+ const dot = join(path, ".git");
103
+ const config = statSync(dot).isDirectory() ? readFileSync(join(dot, "config"), "utf8") : null;
104
+ const url = config === null ? null : /\[remote "origin"\][^[]*?url\s*=\s*(\S+)/.exec(config)?.[1] ?? null;
105
+ const match = url === null ? null : /github\.com[:/]([A-Za-z0-9_.-]+)\/([A-Za-z0-9_.-]+?)(?:\.git)?\/?$/.exec(url);
106
+ return match === null ? null : `${match[1]}/${match[2]}`;
107
+ }
108
+ catch {
109
+ return null;
110
+ }
111
+ }
112
+ /** A trigger's settings, checked whole against the flow it starts cards in. Throws in plain words. */
113
+ export function validateTriggerConfig(raw, context) {
114
+ if (raw === null || typeof raw !== "object" || Array.isArray(raw))
115
+ throw new Error("Choose what starts cards.");
116
+ const input = raw;
117
+ const kind = input["kind"];
118
+ if (!FLOW_TRIGGER_KINDS.includes(kind))
119
+ throw new Error("Choose what starts cards: a button, a schedule, GitHub, Linear, another flow, a webhook, an email inbox or a chat channel.");
120
+ const named = words(input, "zone", 60, false);
121
+ const zoneStage = named === null ? null : context.definition.stages.find(one => one.id === named || one.title.toLowerCase() === named.toLowerCase());
122
+ if (named !== null && zoneStage === undefined)
123
+ throw new Error(`This flow has no zone called ${named}.`);
124
+ const zone = zoneStage?.id ?? null;
125
+ const choice = (key, options, fallback) => {
126
+ const value = input[key];
127
+ if (value === undefined || value === null || value === "")
128
+ return fallback;
129
+ if (!options.includes(value))
130
+ throw new Error(`Choose the trigger's ${key}: ${options.join(" or ")}.`);
131
+ return value;
132
+ };
133
+ switch (kind) {
134
+ case "button": {
135
+ const label = words(input, "label", 40, true);
136
+ const asked = input["questions"];
137
+ const questions = (Array.isArray(asked) ? asked : typeof asked === "string" ? asked.split("\n") : [])
138
+ .map(one => typeof one === "string" ? one.trim() : "").filter(one => one !== "");
139
+ if (questions.length > 6)
140
+ throw new Error("A button asks at most 6 questions.");
141
+ for (const question of questions)
142
+ if (question.length > 80 || scanForSecrets(question).length > 0)
143
+ throw new Error("Each question is a short line of plain text.");
144
+ return { kind: "button", label, questions: questions.length === 0 ? ["What needs doing?", "Details"] : questions, zone };
145
+ }
146
+ case "schedule": {
147
+ const said = words(input, "schedule", 80, true);
148
+ const schedule = scheduleFromWords(said);
149
+ if (schedule === null)
150
+ throw new Error("Say the schedule like “every 2 hours”, “daily 09:00 Europe/London”, “weekdays 09:00” or “monday 09:00”.");
151
+ // A script on a schedule (v90): each item it prints becomes a card.
152
+ const script = words(input, "script", 40, false);
153
+ if (script !== null) {
154
+ if (context.store.flowScript(context.flow.repo, script) === null)
155
+ throw new Error(`There's no script called ${script} in this project. Make it on the flow's Scripts panel first.`);
156
+ const asked = input["secrets"];
157
+ const secrets = asked === undefined || asked === null ? [] : Array.isArray(asked) ? asked : typeof asked === "string" ? asked.split(/[\s,]+/).filter(one => one !== "") : null;
158
+ if (secrets === null || secrets.length > 10 || secrets.some(one => typeof one !== "string" || !/^[A-Z][A-Z0-9_]{0,39}$/.test(one)))
159
+ throw new Error("Name up to 10 saved secrets in capitals, like API_TOKEN.");
160
+ return { kind: "schedule", schedule, title: `Items from ${script}`, description: null, zone, script, ...(secrets.length === 0 ? {} : { secrets: [...new Set(secrets)] }) };
161
+ }
162
+ return { kind: "schedule", schedule, title: words(input, "title", 200, true), description: words(input, "description", 2000, false), zone };
163
+ }
164
+ case "github": {
165
+ const repo = words(input, "repo", 140, false) ?? githubRepoOf(context.flow.repo);
166
+ if (repo === null || !/^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/.test(repo))
167
+ throw new Error("Name the GitHub repository as owner/name.");
168
+ const watch = choice("watch", ["issues", "pulls", "checks"], "issues");
169
+ const label = watch === "checks" ? null : words(input, "label", 50, false);
170
+ const branch = watch === "checks" ? words(input, "branch", 100, false) ?? "main" : null;
171
+ if (branch !== null && !/^[A-Za-z0-9._/-]+$/.test(branch))
172
+ throw new Error("That branch name isn't valid.");
173
+ return { kind: "github", repo, watch, label, branch, from: choice("from", ["team", "anyone"], "team"), delivery: choice("delivery", ["poll", "webhook"], "poll"), zone };
174
+ }
175
+ case "linear": {
176
+ const team = words(input, "team", 12, false), state = words(input, "state", 40, false), label = words(input, "label", 50, false);
177
+ if (team !== null && !/^[A-Za-z0-9]+$/.test(team))
178
+ throw new Error("A Linear team is its short key, like ENG.");
179
+ if (team === null && label === null)
180
+ throw new Error("Name a Linear team or a label, so the trigger doesn't take every issue in the workspace.");
181
+ return { kind: "linear", team: team?.toUpperCase() ?? null, state, label, delivery: choice("delivery", ["poll", "webhook"], "poll"), zone };
182
+ }
183
+ case "flow": {
184
+ const source = Number(input["flow"]);
185
+ const flow = Number.isSafeInteger(source) ? context.store.getFlow(source) : null;
186
+ const sourceDefinition = flow === null ? null : flowDefinitionOf(flow);
187
+ if (flow === null || flow.state !== "active" || sourceDefinition === null || !context.store.accountCanAccess(context.actor, flow.repo))
188
+ throw new Error("Choose another flow in your projects.");
189
+ if (flow.id === context.flow.id)
190
+ throw new Error("A flow can't start cards in itself; move them with a zone's next step instead.");
191
+ const when = words(input, "when", 60, false);
192
+ const stage = when === null ? sourceDefinition.stages.find(one => one.kind === "done") : sourceDefinition.stages.find(one => one.id === when || one.title.toLowerCase() === when.toLowerCase());
193
+ if (stage === undefined)
194
+ throw new Error(`${flow.name} has no zone called ${when ?? "Done"}.`);
195
+ return { kind: "flow", flow: flow.id, when: stage.id, zone };
196
+ }
197
+ case "webhook":
198
+ return { kind: "webhook", title: words(input, "title", 120, false) ?? "Webhook", titleField: words(input, "titleField", 80, false), bodyField: words(input, "bodyField", 80, false), zone };
199
+ case "chat":
200
+ // Connecting a channel proves it's one the person is in: it happens in the channel, not here.
201
+ 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}”.`);
202
+ case "email": {
203
+ const folder = words(input, "folder", 100, false) ?? "INBOX";
204
+ const sender = words(input, "sender", 300, false);
205
+ if (sender !== null && sendersOf(sender).length === 0)
206
+ throw new Error("Say whom mail comes from as addresses or domains, like priya@example.com or example.com.");
207
+ return { kind: "email", folder: /^inbox$/i.test(folder) ? "INBOX" : folder, sender: sender === null ? null : sendersOf(sender).join(", "), subject: words(input, "subject", 100, false), zone };
208
+ }
209
+ }
210
+ }
211
+ /** Addresses and domains from "priya@example.com, @shop.com, example.org". */
212
+ function sendersOf(text) {
213
+ return [...new Set(text.split(/[\s,;]+/).map(one => one.trim().toLowerCase().replace(/^@/, "")).filter(one => /^([a-z0-9._%+-]+@)?[a-z0-9-]+(\.[a-z0-9-]+)*\.[a-z]{2,}$/.test(one)))].slice(0, 20);
214
+ }
215
+ /** Whether an email is one this trigger takes: from a named sender (or domain), with the words in its subject. */
216
+ export function mailMatches(config, mail) {
217
+ const from = mail.from.toLowerCase();
218
+ const senders = config.sender === null ? [] : sendersOf(config.sender);
219
+ if (senders.length > 0 && !senders.some(one => one.includes("@") ? from === one : from.endsWith(`@${one}`) || from.endsWith(`.${one}`)))
220
+ return false;
221
+ return config.subject === null || mail.subject.toLowerCase().includes(config.subject.toLowerCase());
222
+ }
223
+ /** The trigger in the words the canvas and its cards use. */
224
+ export function describeTrigger(config, store) {
225
+ const from = (value) => value === "team" ? ", from people with write access" : ", from anyone";
226
+ const way = (delivery) => delivery === "poll" ? " (checked every 2 minutes)" : " (sent to its webhook address)";
227
+ switch (config.kind) {
228
+ case "button": return `The “${config.label}” button asks: ${config.questions.join(" · ")}`;
229
+ case "schedule": {
230
+ const schedule = parseSchedule(config.schedule);
231
+ const when = schedule === null ? config.schedule : describeSchedule(schedule);
232
+ return config.script !== undefined ? `${when.charAt(0).toUpperCase()}${when.slice(1)}: runs the ${config.script} script, and each item it prints becomes a card` : `${when.charAt(0).toUpperCase()}${when.slice(1)}: “${config.title}”`;
233
+ }
234
+ case "github":
235
+ return config.watch === "checks" ? `Failed checks on ${config.branch} in ${config.repo}${way(config.delivery)}`
236
+ : `${config.watch === "issues" ? "GitHub issues" : "Pull requests"} in ${config.repo}${config.label === null ? "" : ` labeled ${config.label}`}${from(config.from)}${way(config.delivery)}`;
237
+ case "linear":
238
+ return `Linear issues${config.team === null ? "" : ` in ${config.team}`}${config.state === null ? "" : ` that move to ${config.state}`}${config.label === null ? "" : ` labeled ${config.label}`}${way(config.delivery)}`;
239
+ case "flow": {
240
+ const flow = store.getFlow(config.flow);
241
+ const zone = flow === null ? null : flowDefinitionOf(flow)?.stages.find(one => one.id === config.when)?.title ?? null;
242
+ return `Cards reaching ${zone ?? "the end"} in ${flow?.name ?? "another flow"}`;
243
+ }
244
+ case "webhook": return "Anything posted to its webhook address";
245
+ case "chat": return `Every new message in a ${CHAT_APP_NAMES[config.app]} channel; replies in a card's thread join its discussion`;
246
+ case "email": return `Email arriving in ${config.folder === "INBOX" ? "the inbox" : config.folder}${config.sender === null ? "" : ` from ${config.sender}`}${config.subject === null ? "" : ` with “${config.subject}” in the subject`} (checked every 2 minutes)`;
247
+ }
248
+ }
249
+ /** The trigger in two short lines for the canvas: what it is, and the one detail that tells it apart. */
250
+ export function triggerHeadline(config, store) {
251
+ switch (config.kind) {
252
+ case "button": return { name: config.label, detail: `Asks ${config.questions.length} question${config.questions.length === 1 ? "" : "s"}` };
253
+ case "schedule": {
254
+ const schedule = parseSchedule(config.schedule);
255
+ const when = schedule === null ? config.schedule : schedule.kind === "every" ? describeSchedule(schedule) : `${schedule.kind === "daily" ? "daily" : schedule.kind === "weekdays" ? "weekdays" : `${WEEKDAYS[schedule.day]}s`} at ${schedule.hhmm}`;
256
+ return { name: `${when.charAt(0).toUpperCase()}${when.slice(1)}`, detail: config.script !== undefined ? `Runs ${config.script}` : `“${config.title}”` };
257
+ }
258
+ case "github": return { name: config.watch === "checks" ? `Failed checks on ${config.branch}` : config.watch === "pulls" ? `New pull requests${config.label === null ? "" : ` labeled ${config.label}`}` : config.label === null ? "New issues" : `Issues labeled ${config.label}`, detail: `GitHub · ${config.repo}` };
259
+ case "linear": return { name: `Linear${config.team === null ? "" : ` ${config.team}`}${config.state === null ? "" : ` → ${config.state}`}`, detail: config.label === null ? "Linear issues" : `Labeled ${config.label}` };
260
+ case "flow": {
261
+ const flow = store.getFlow(config.flow);
262
+ return { name: `From ${flow?.name ?? "another flow"}`, detail: `When a card reaches ${flow === null ? "the end" : flowDefinitionOf(flow)?.stages.find(one => one.id === config.when)?.title ?? "the end"}` };
263
+ }
264
+ case "webhook": return { name: "Webhook", detail: "Anything posted to its address" };
265
+ case "chat": return { name: `${CHAT_APP_NAMES[config.app]} channel`, detail: "Each message is a card" };
266
+ case "email": return { name: config.folder === "INBOX" ? "New email" : `New email in ${config.folder}`, detail: config.sender !== null ? `From ${config.sender}` : config.subject !== null ? `Subject has “${config.subject}”` : "Email inbox" };
267
+ }
268
+ }
269
+ export function triggerConfigOf(trigger) {
270
+ try {
271
+ return JSON.parse(trigger.configJson);
272
+ }
273
+ catch {
274
+ return null;
275
+ }
276
+ }
277
+ /** Whether this trigger takes deliveries at a secret address rather than being checked. */
278
+ export function takesDeliveries(config) {
279
+ return config.kind === "webhook" || ((config.kind === "github" || config.kind === "linear") && config.delivery === "webhook");
280
+ }
281
+ // ------------------------------------------------------ secrets on disk
282
+ const hookHash = (token) => createHash("sha256").update(token).digest("hex");
283
+ const secretFile = (dir, trigger) => join(dir, "flow-hooks", `${trigger}.json`);
284
+ /** A webhook's signing secret (GitHub: made here; Linear: pasted from Linear). 0600, beside the database, never a column. */
285
+ export function saveHookSecret(dir, trigger, secret) {
286
+ mkdirSync(join(dir, "flow-hooks"), { recursive: true, mode: 0o700 });
287
+ writeFileSync(secretFile(dir, trigger), `${JSON.stringify({ secret })}\n`, { mode: 0o600 });
288
+ chmodSync(secretFile(dir, trigger), 0o600);
289
+ }
290
+ export function readHookSecret(dir, trigger) {
291
+ try {
292
+ const parsed = JSON.parse(readFileSync(secretFile(dir, trigger), "utf8"));
293
+ return typeof parsed.secret === "string" && parsed.secret !== "" ? parsed.secret : null;
294
+ }
295
+ catch {
296
+ return null;
297
+ }
298
+ }
299
+ function dropHookSecret(dir, trigger) { rmSync(secretFile(dir, trigger), { force: true }); }
300
+ const LINEAR_FILE = "linear-key";
301
+ export const LINEAR_ENV = "TOOLROLL_LINEAR_KEY";
302
+ /** The Linear API key the worker checks Linear with: the environment, or a 0600 file beside the database. */
303
+ export function readLinearKey(dir) {
304
+ const env = envValue(process.env, "LINEAR_KEY")?.trim();
305
+ if (env)
306
+ return env;
307
+ if (dir === null)
308
+ return null;
309
+ try {
310
+ const key = readFileSync(join(dir, LINEAR_FILE), "utf8").trim();
311
+ return key === "" ? null : key;
312
+ }
313
+ catch {
314
+ return null;
315
+ }
316
+ }
317
+ export function saveLinearKey(dir, key) {
318
+ const clean = key.trim();
319
+ if (!/^lin_(api|oauth)_[A-Za-z0-9]{20,}$/.test(clean))
320
+ return { ok: false, message: "That doesn't look like a Linear API key. Copy it from Linear → Settings → Security & access → Personal API keys." };
321
+ writeFileSync(join(dir, LINEAR_FILE), `${clean}\n`, { mode: 0o600 });
322
+ chmodSync(join(dir, LINEAR_FILE), 0o600);
323
+ return { ok: true };
324
+ }
325
+ export function removeLinearKey(dir) { rmSync(join(dir, LINEAR_FILE), { force: true }); }
326
+ const HOOKS_FILE = "hooks-url";
327
+ /** The public address webhooks reach this console at (a reverse proxy such as Caddy, exposing /hooks/ only). */
328
+ export function readHooksBase(dir) {
329
+ if (dir === null)
330
+ return null;
331
+ try {
332
+ const base = readFileSync(join(dir, HOOKS_FILE), "utf8").trim();
333
+ return base === "" ? null : base;
334
+ }
335
+ catch {
336
+ return null;
337
+ }
338
+ }
339
+ export function saveHooksBase(dir, value) {
340
+ const clean = value.trim().replace(/\/+$/, "");
341
+ if (clean === "") {
342
+ rmSync(join(dir, HOOKS_FILE), { force: true });
343
+ return { ok: true, base: null };
344
+ }
345
+ let url;
346
+ try {
347
+ url = new URL(clean);
348
+ }
349
+ catch {
350
+ return { ok: false, message: "Give the address as https://your-domain." };
351
+ }
352
+ if (url.protocol !== "https:" || url.search !== "" || url.hash !== "" || url.username !== "" || url.password !== "")
353
+ return { ok: false, message: "Give the address as https://your-domain, without a path's query or a password." };
354
+ writeFileSync(join(dir, HOOKS_FILE), `${url.origin}${url.pathname === "/" ? "" : url.pathname.replace(/\/+$/, "")}\n`, { mode: 0o600 });
355
+ return { ok: true, base: readHooksBase(dir) };
356
+ }
357
+ export const HOOK_PATH = "/hooks/flow/";
358
+ /** Add a trigger to a flow. A webhook trigger's address (and GitHub's signing secret) is returned once and never stored readable. */
359
+ export function addFlowTriggerTo(store, flow, raw, actor, now, dir) {
360
+ const definition = flowDefinitionOf(flow);
361
+ if (definition === null || flow.state !== "active")
362
+ return { ok: false, message: "This flow can't take triggers right now." };
363
+ let config;
364
+ try {
365
+ config = validateTriggerConfig(raw, { store, flow, definition, actor });
366
+ }
367
+ catch (error) {
368
+ return { ok: false, message: error instanceof Error ? error.message : "That trigger isn't valid." };
369
+ }
370
+ if (takesDeliveries(config) && dir === null)
371
+ return { ok: false, message: "Webhook addresses are set up on the console." };
372
+ const token = takesDeliveries(config) ? randomBytes(24).toString("base64url") : null;
373
+ const schedule = config.kind === "schedule" ? parseSchedule(config.schedule) : null;
374
+ const id = store.addFlowTrigger({
375
+ flow: flow.id, kind: config.kind, configJson: JSON.stringify(config), hookHash: token === null ? null : hookHash(token),
376
+ // Only what happens from now on: never the backlog of issues, runs or finished cards.
377
+ cursor: config.kind === "flow" ? String(store.latestFlowEvent()) : config.kind === "github" || config.kind === "linear" ? now.toISOString() : null,
378
+ // Email: the first check notes where the mailbox stands (no cursor yet), so only mail after this becomes cards.
379
+ nextAt: schedule !== null ? firstFireAt(schedule, now) : polled(config) ? now.toISOString() : null,
380
+ by: actor,
381
+ }, now);
382
+ let secret = null;
383
+ if (config.kind === "github" && config.delivery === "webhook") {
384
+ secret = randomBytes(24).toString("hex");
385
+ saveHookSecret(dir, id, secret);
386
+ }
387
+ const reveal = token === null ? null : { path: `${HOOK_PATH}${token}`, address: dir === null || readHooksBase(dir) === null ? null : `${readHooksBase(dir)}${HOOK_PATH}${token}`, secret };
388
+ return { ok: true, id, said: token === null ? "Trigger added." : "Trigger added. Copy its address now; it isn't shown again.", reveal };
389
+ }
390
+ /** A new secret address for a webhook trigger (the old one stops working at once). */
391
+ export function renewFlowHook(store, trigger, now, dir) {
392
+ const config = triggerConfigOf(trigger);
393
+ if (config === null || !takesDeliveries(config) || trigger.state === "removed")
394
+ return { ok: false, message: "That trigger has no webhook address." };
395
+ const token = randomBytes(24).toString("base64url");
396
+ store.updateFlowTrigger(trigger.id, { hookHash: hookHash(token) }, now);
397
+ let secret = null;
398
+ if (config.kind === "github") {
399
+ secret = randomBytes(24).toString("hex");
400
+ saveHookSecret(dir, trigger.id, secret);
401
+ }
402
+ const base = readHooksBase(dir);
403
+ return { ok: true, id: trigger.id, said: "New address made; the old one no longer works. Copy it now; it isn't shown again.", reveal: { path: `${HOOK_PATH}${token}`, address: base === null ? null : `${base}${HOOK_PATH}${token}`, secret } };
404
+ }
405
+ export function removeFlowTrigger(store, trigger, now, dir) {
406
+ store.updateFlowTrigger(trigger.id, { state: "removed", hookHash: null }, now);
407
+ if (dir !== null)
408
+ dropHookSecret(dir, trigger.id);
409
+ }
410
+ /** Checked by the worker's pass (rather than pushed, fired on a schedule or followed). */
411
+ function polled(config) {
412
+ return config.kind === "email" || ((config.kind === "github" || config.kind === "linear") && config.delivery === "poll");
413
+ }
414
+ function startZone(config, definition) {
415
+ return config.zone !== null && definition.stages.some(one => one.id === config.zone) ? config.zone : definition.start;
416
+ }
417
+ /** One outside thing into one card, at most once per trigger. Keys and control characters never reach a card. */
418
+ function makeCard(store, trigger, config, item, by, now) {
419
+ const flow = store.getFlow(trigger.flow);
420
+ const definition = flow === null ? null : flowDefinitionOf(flow);
421
+ if (flow === null || definition === null || flow.state !== "active")
422
+ return { made: "skipped", note: "the flow isn't active", card: null };
423
+ if (store.flowTriggerSaw(trigger.id, item.key))
424
+ return { made: "seen", note: null, card: null };
425
+ const text = flowCardText(item.title, item.description);
426
+ if ("problem" in text) {
427
+ const note = text.problem.startsWith("That looks like a key") ? "it looked like it held a key or password" : text.problem;
428
+ store.recordFlowTriggerEvent(trigger.id, item.key, null, note, now);
429
+ return { made: "skipped", note: `${item.source.label}: ${note}`, card: null };
430
+ }
431
+ return store.transact(() => {
432
+ const card = store.addFlowCard({ flow: flow.id, title: text.title, description: text.description, stage: startZone(config, definition), by, source: item.source }, now);
433
+ store.recordFlowTriggerEvent(trigger.id, item.key, card, null, now);
434
+ return { made: "added", note: null, card };
435
+ });
436
+ }
437
+ const plural = (n, word) => `${n} ${word}${n === 1 ? "" : "s"}`;
438
+ function outcomeWords(results, more) {
439
+ const added = results.filter(one => one.made === "added").length;
440
+ const skipped = results.filter(one => one.made === "skipped");
441
+ const parts = [added > 0 ? `Added ${plural(added, "card")}.` : skipped.length > 0 ? "No new cards." : "Nothing new."];
442
+ if (skipped.length > 0)
443
+ parts.push(`Left out ${plural(skipped.length, "item")}: ${skipped[0].note}${skipped.length > 1 ? ", and others" : ""}.`);
444
+ if (more)
445
+ parts.push("More on the next check.");
446
+ return parts.join(" ");
447
+ }
448
+ /** One outside thing into one card through a trigger (a chat channel's message, v89): at most once per key. */
449
+ export function addTriggerCard(store, trigger, item, by, now) {
450
+ const config = triggerConfigOf(trigger);
451
+ if (config === null || trigger.state !== "active")
452
+ return { made: "skipped", note: "the trigger isn't active", card: null };
453
+ return makeCard(store, trigger, config, item, by, now);
454
+ }
455
+ /** Press a button trigger: the first answer is the card's title, the rest its details. */
456
+ export function pressFlowButton(store, trigger, answers, actor, now) {
457
+ const config = triggerConfigOf(trigger);
458
+ if (config?.kind !== "button" || trigger.state !== "active")
459
+ return { ok: false, message: "That button isn't available." };
460
+ const said = config.questions.map((_question, index) => typeof answers[index] === "string" ? answers[index].trim() : "");
461
+ if (said[0] === "")
462
+ return { ok: false, message: `Answer “${config.questions[0]}”.` };
463
+ const details = config.questions.slice(1).map((question, index) => said[index + 1] === "" ? null : `${question}\n${said[index + 1]}`).filter(one => one !== null).join("\n\n");
464
+ const result = makeCard(store, trigger, config, { key: `press:${now.toISOString()}:${randomUUID().slice(0, 8)}`, title: said[0], description: details === "" ? null : details, source: { kind: "button", label: config.label, url: null } }, actor, now);
465
+ if (result.made !== "added")
466
+ return { ok: false, message: result.note ?? "That didn't make a card." };
467
+ store.setFlowCardWatcher(result.card, actor, true, now);
468
+ store.updateFlowTrigger(trigger.id, { lastAt: now.toISOString(), lastOutcome: `Pressed by ${actor}.` }, now);
469
+ return { ok: true, said: "Card added.", card: result.card };
470
+ }
471
+ /** Every trigger in one project that is due: schedules and other flows here, GitHub, Linear and email by asking them. */
472
+ export async function runFlowTriggers(store, repo, now, io) {
473
+ const pass = { added: 0, checked: 0, problems: [] };
474
+ for (const trigger of store.activeFlowTriggers(repo)) {
475
+ const config = triggerConfigOf(trigger);
476
+ if (config === null)
477
+ continue;
478
+ try {
479
+ if (config.kind === "schedule" && config.script !== undefined && trigger.nextAt !== null && Date.parse(trigger.nextAt) <= now.getTime()) {
480
+ const fired = await fireScript(store, trigger, config, now, io, true);
481
+ pass.added += fired.added;
482
+ if (!fired.ok)
483
+ pass.problems.push(`trigger ${trigger.id}: ${fired.said}`);
484
+ }
485
+ else if (config.kind === "schedule" && trigger.nextAt !== null && Date.parse(trigger.nextAt) <= now.getTime())
486
+ pass.added += fireSchedule(store, trigger, config, now);
487
+ else if (config.kind === "flow")
488
+ pass.added += followFlow(store, trigger, config, now);
489
+ else if (polled(config) && (trigger.nextAt === null || Date.parse(trigger.nextAt) <= now.getTime())) {
490
+ pass.checked++;
491
+ const checked = await checkTrigger(store, trigger, config, now, io);
492
+ pass.added += checked.added;
493
+ if (!checked.ok)
494
+ pass.problems.push(`trigger ${trigger.id}: ${checked.said}`);
495
+ }
496
+ }
497
+ catch (error) {
498
+ pass.problems.push(`trigger ${trigger.id}: ${error instanceof Error ? error.message : "could not run"}`);
499
+ }
500
+ }
501
+ return pass;
502
+ }
503
+ /** "Check now" on the canvas: one trigger, right away, in words. */
504
+ export async function checkFlowTriggerNow(store, trigger, now, io) {
505
+ const config = triggerConfigOf(trigger);
506
+ // A schedule's script runs now, once; its schedule stays as it is.
507
+ if (config?.kind === "schedule" && config.script !== undefined) {
508
+ const fired = await fireScript(store, trigger, config, now, io, false);
509
+ return { ok: fired.ok, said: fired.said };
510
+ }
511
+ if (config === null || !polled(config))
512
+ return { ok: false, said: "Only GitHub, Linear and email triggers that are checked, and schedules that run a script, can be run now." };
513
+ const checked = await checkTrigger(store, trigger, config, now, io);
514
+ return { ok: checked.ok, said: checked.said };
515
+ }
516
+ function fireSchedule(store, trigger, config, now) {
517
+ const schedule = parseSchedule(config.schedule);
518
+ if (schedule === null || trigger.nextAt === null)
519
+ return 0;
520
+ const slot = trigger.nextAt;
521
+ const next = nextFireAt(schedule, slot, now);
522
+ // One card at a time: while the last one still waits where it started, the slot is skipped, not piled up.
523
+ const last = store.lastFlowTriggerCard(trigger.id);
524
+ const card = last === null ? null : store.getFlowCard(last);
525
+ const flow = store.getFlow(trigger.flow);
526
+ const definition = flow === null ? null : flowDefinitionOf(flow);
527
+ if (card !== null && definition !== null && card.state === "active" && card.stage === startZone(config, definition) && card.entry === 1) {
528
+ store.recordFlowTriggerEvent(trigger.id, `slot:${slot}`, null, "the last one hadn't been picked up", now);
529
+ store.updateFlowTrigger(trigger.id, { nextAt: next, lastAt: now.toISOString(), lastOutcome: "Skipped: the last card hasn't been picked up yet." }, now);
530
+ return 0;
531
+ }
532
+ const day = new Intl.DateTimeFormat("en-US", { month: "short", day: "numeric", timeZone: schedule.kind === "every" ? "UTC" : schedule.timezone ?? "UTC" }).format(new Date(slot));
533
+ const fill = (text) => text.replace(/\{\{\s*date\s*\}\}/g, day);
534
+ const title = /\{\{\s*date\s*\}\}/.test(config.title) ? fill(config.title) : `${config.title} — ${day}`;
535
+ const made = makeCard(store, trigger, config, { key: `slot:${slot}`, title, description: config.description === null ? null : fill(config.description), source: { kind: "schedule", label: "Schedule", url: null } }, "Schedule", now);
536
+ store.updateFlowTrigger(trigger.id, { nextAt: next, lastAt: now.toISOString(), lastOutcome: made.made === "added" ? "Added a card." : made.note ?? "Nothing new." }, now);
537
+ return made.made === "added" ? 1 : 0;
538
+ }
539
+ /** v96: a schedule (a teammate's routine) makes its card now, once per press, and keeps its own times. */
540
+ export function runScheduleNow(store, trigger, actor, now) {
541
+ const config = triggerConfigOf(trigger);
542
+ if (config?.kind !== "schedule" || config.script !== undefined)
543
+ return { ok: false, said: "Only a schedule without a script runs this way." };
544
+ if (trigger.state !== "active")
545
+ return { ok: false, said: "It's paused." };
546
+ const made = makeCard(store, trigger, config, { key: `now:${now.toISOString()}`, title: config.title.replace(/\{\{\s*date\s*\}\}/g, "today"), description: config.description, source: { kind: "schedule", label: `Run now by ${actor}`, url: null } }, "Schedule", now);
547
+ if (made.made !== "added" || made.card === null)
548
+ return { ok: false, said: made.note ?? "Nothing was added." };
549
+ store.updateFlowTrigger(trigger.id, { lastAt: now.toISOString(), lastOutcome: "Added a card (run now)." }, now);
550
+ return { ok: true, card: made.card };
551
+ }
552
+ /** A schedule's script (v90): it runs in a clean folder inside the agents' fence; each item it prints is a card, once. */
553
+ async function fireScript(store, trigger, config, now, io, scheduled) {
554
+ const schedule = parseSchedule(config.schedule);
555
+ const next = scheduled && schedule !== null && trigger.nextAt !== null ? nextFireAt(schedule, trigger.nextAt, now) : trigger.nextAt;
556
+ const flow = store.getFlow(trigger.flow);
557
+ const done = (ok, said, added = 0) => {
558
+ store.updateFlowTrigger(trigger.id, { nextAt: next, lastAt: now.toISOString(), lastOutcome: said, failures: ok ? 0 : trigger.failures + 1 }, now);
559
+ return { ok, said, added };
560
+ };
561
+ const script = flow === null ? null : store.flowScript(flow.repo, config.script);
562
+ if (flow === null || script === null)
563
+ return done(false, `There's no script called ${config.script} in this project any more.`);
564
+ const saved = readFlowSecrets(io.dir, flow.repo);
565
+ const missing = (config.secrets ?? []).filter(name => saved[name] === undefined);
566
+ if (missing.length > 0)
567
+ return done(false, `${script.name} needs the secret${missing.length === 1 ? "" : "s"} ${missing.join(", ")}. Save ${missing.length === 1 ? "it" : "them"} on the flow first.`);
568
+ // Loaded when a script first runs: the runner reaches the builder's modules, which reach back to these triggers.
569
+ const { cardsFromOutput, cleanFolder, runCode } = await import("./flow-code.js");
570
+ const { agentFence } = await import("./agent-fence.js");
571
+ const scratch = io.scratch ?? join(io.dir ?? tmpdir(), "flow-scratch");
572
+ const folder = cleanFolder(scratch, `flow-trigger-${trigger.id}`);
573
+ let ran;
574
+ try {
575
+ ran = await runCode({ script, cwd: folder, root: flow.repo, scratch, shell: io.shell ?? io.gh,
576
+ input: { flow: { id: flow.id, name: flow.name }, trigger: trigger.id, lastRun: trigger.lastAt }, secrets: Object.fromEntries((config.secrets ?? []).map(name => [name, saved[name]])),
577
+ env: { FLOW_NAME: flow.name, FLOW_TRIGGER_ID: String(trigger.id), FLOW_PROJECT: flow.repo, FLOW_LAST_RUN: trigger.lastAt ?? "" },
578
+ fence: agentFence({ databaseFile: store.databaseFile(), worktree: folder }) });
579
+ }
580
+ finally {
581
+ rmSync(folder, { recursive: true, force: true });
582
+ }
583
+ if (ran.state === "failed")
584
+ return done(false, ran.said.split("\n")[0]);
585
+ const items = cardsFromOutput(ran.printed);
586
+ const results = items.slice(0, CARDS_PER_CHECK).map(item => makeCard(store, trigger, config, {
587
+ key: `script:${item.key}`, title: item.title, description: item.description, source: { kind: "script", label: `The ${script.name} script`, url: null },
588
+ }, `The ${script.name} script`, now));
589
+ return done(true, items.length === 0 ? `${script.name} ran and printed nothing to add.` : outcomeWords(results, items.length > CARDS_PER_CHECK), results.filter(one => one.made === "added").length);
590
+ }
591
+ function followFlow(store, trigger, config, now) {
592
+ const source = store.getFlow(config.flow);
593
+ const after = Number(trigger.cursor ?? 0);
594
+ const arrivals = source === null ? [] : store.flowArrivals(source.id, config.when, after, CARDS_PER_CHECK);
595
+ if (arrivals.length === 0)
596
+ return 0;
597
+ const results = arrivals.map(({ card }) => makeCard(store, trigger, config, {
598
+ key: `card:${card.id}`, title: card.title, description: card.description,
599
+ source: { kind: "flow", label: `${source.name}: ${card.title}`.slice(0, 120), url: `/flows/${source.id}?card=${card.id}` },
600
+ }, `the ${source.name} flow`, now));
601
+ store.updateFlowTrigger(trigger.id, { cursor: String(arrivals.at(-1).event), lastAt: now.toISOString(), lastOutcome: outcomeWords(results, arrivals.length === CARDS_PER_CHECK) }, now);
602
+ return results.filter(one => one.made === "added").length;
603
+ }
604
+ async function checkTrigger(store, trigger, config, now, io) {
605
+ const fetched = config.kind === "github" ? await fetchGitHub(config, trigger, io.gh) : config.kind === "linear" ? await fetchLinear(config, trigger, io) : await fetchMail(store, config, trigger, io, now);
606
+ if (!fetched.ok) {
607
+ const failures = trigger.failures + 1;
608
+ store.updateFlowTrigger(trigger.id, { failures, lastAt: now.toISOString(), lastOutcome: fetched.problem, nextAt: new Date(now.getTime() + BACKOFF_MS[Math.min(failures, BACKOFF_MS.length) - 1]).toISOString() }, now);
609
+ return { ok: false, said: fetched.problem, added: 0 };
610
+ }
611
+ const by = config.kind === "github" ? "GitHub" : config.kind === "linear" ? "Linear" : "Email";
612
+ const due = fetched.items.slice(0, CARDS_PER_CHECK);
613
+ const results = due.map(item => "skip" in item
614
+ ? (store.recordFlowTriggerEvent(trigger.id, item.key, null, item.skip, now) ? { made: "skipped", note: `${item.label}: ${item.skip}`, card: null } : { made: "seen", note: null, card: null })
615
+ : makeCard(store, trigger, config, item, by, now));
616
+ // A mailbox says itself whether more is waiting; its cursor is already just past what it read.
617
+ const more = fetched.more ?? fetched.items.length > CARDS_PER_CHECK;
618
+ // The cursor moves past what was handled, never past what is still waiting.
619
+ const cursor = fetched.more !== undefined ? fetched.cursor ?? trigger.cursor : more ? due.at(-1)?.at ?? trigger.cursor : fetched.cursor ?? trigger.cursor;
620
+ const said = results.length === 0 && fetched.said !== undefined ? fetched.said : outcomeWords(results, more);
621
+ store.updateFlowTrigger(trigger.id, { failures: 0, cursor, lastAt: now.toISOString(), lastOutcome: said, nextAt: new Date(now.getTime() + POLL_EVERY_MS).toISOString() }, now);
622
+ return { ok: true, said, added: results.filter(one => one.made === "added").length };
623
+ }
624
+ /** New mail in the folder since the last check, each message a card (or why it was left out). */
625
+ async function fetchMail(store, config, trigger, io, now) {
626
+ const signed = await mailboxAccess(io.dir, io.fetch);
627
+ if (!signed.ok)
628
+ return { ok: false, problem: signed.said };
629
+ const after = mailCursorOf(trigger.cursor);
630
+ const read = await (io.mail ?? readThroughImap)(signed.access, config.folder, after, CARDS_PER_CHECK);
631
+ if (!read.ok)
632
+ return { ok: false, problem: read.said };
633
+ const own = signed.address.toLowerCase();
634
+ const items = read.mails.filter(mail => mailMatches(config, mail)).map(mail => {
635
+ const key = `mail:${read.at.validity}:${mail.uid}`, at = mailCursorText({ validity: read.at.validity, uid: mail.uid });
636
+ const label = `Email from ${mail.from || "an unknown sender"}`.slice(0, 120);
637
+ // Machines and this account itself never start cards: a flow that answers mail mustn't answer them.
638
+ if (mail.automatic)
639
+ return { key, skip: "an automatic reply", label, at };
640
+ if (mail.from === own)
641
+ return { key, skip: "sent from this account", label, at };
642
+ if (mail.from === "")
643
+ return { key, skip: "it had no sender", label, at };
644
+ // A reply to a card's email (v91) joins that card instead of starting another.
645
+ const reply = takeReply(store, mail, own, now);
646
+ if (reply !== null)
647
+ return { key, skip: `a reply to “${reply.card.title}”`, label, at };
648
+ const who = mail.fromName === null ? mail.from : `${mail.fromName} <${mail.from}>`;
649
+ return { key, at, title: mail.subject || `Email from ${mail.fromName ?? mail.from}`, description: `From: ${who}${mail.text === "" ? "" : `\n\n${mail.text}`}`,
650
+ source: { kind: "email", label, url: null, mail: { id: mail.messageId, references: mail.references, subject: mail.subject, from: mail.from } } };
651
+ });
652
+ const said = after === null ? `Watching ${config.folder === "INBOX" ? "the inbox" : config.folder} of ${signed.address}: new email from now on becomes cards.`
653
+ : read.renumbered ? "The mail server renumbered this folder; watching it from now." : undefined;
654
+ return { ok: true, items, cursor: mailCursorText(read.at), more: read.more, ...(said === undefined ? {} : { said }) };
655
+ }
656
+ const text = (value) => typeof value === "string" ? value : "";
657
+ const record = (value) => value !== null && typeof value === "object" && !Array.isArray(value) ? value : {};
658
+ /** A GitHub issue or pull request as a card, or why it is left out; null when it doesn't match. */
659
+ export function githubItem(config, raw, what) {
660
+ const item = record(raw);
661
+ const number = Number(item["number"]);
662
+ if (!Number.isSafeInteger(number))
663
+ return null;
664
+ if (what === "issue" && item["pull_request"] !== undefined)
665
+ return null;
666
+ if (what === "pull" && item["draft"] === true)
667
+ return null;
668
+ const labels = Array.isArray(item["labels"]) ? item["labels"].map(one => text(record(one)["name"]).toLowerCase()) : [];
669
+ if (config.label !== null && !labels.includes(config.label.toLowerCase()))
670
+ return null;
671
+ const label = what === "issue" ? `GitHub issue #${number}` : `Pull request #${number}`;
672
+ const key = `${what}:${number}`, at = text(item["updated_at"]) || null;
673
+ if (config.from === "team" && !TEAM.has(text(item["author_association"])))
674
+ return { key, skip: "opened by someone without write access", label, at };
675
+ const login = text(record(item["user"])["login"]) || "someone";
676
+ const branches = what === "pull" ? ` (${text(record(item["head"])["ref"])} → ${text(record(item["base"])["ref"])})` : "";
677
+ return { key, at, title: text(item["title"]) || label,
678
+ description: `From ${what === "issue" ? "GitHub issue" : "pull request"} #${number} by @${login}${branches}:\n\n${text(item["body"])}`.trim().slice(0, 4000),
679
+ source: { kind: "github", label, url: text(item["html_url"]) || null } };
680
+ }
681
+ /** A failed workflow run as a card: one card per failing commit on the watched branch. */
682
+ export function githubRunItem(config, raw) {
683
+ const run = record(raw);
684
+ if (text(run["conclusion"]) !== "failure" || (config.branch !== null && text(run["head_branch"]) !== config.branch))
685
+ return null;
686
+ const sha = text(run["head_sha"]);
687
+ if (sha === "")
688
+ return null;
689
+ const name = text(run["name"]) || "Checks";
690
+ const commit = text(run["display_title"]);
691
+ return { key: `checks:${sha}`, at: text(run["created_at"]) || null, title: `Checks failed on ${config.branch}: ${name}`,
692
+ description: `From GitHub: “${name}” failed on ${config.branch} at ${sha.slice(0, 7)}${commit === "" ? "" : ` (${commit})`}.\nRun: ${text(run["html_url"])}`,
693
+ source: { kind: "github", label: `Failed check on ${config.branch}`, url: text(run["html_url"]) || null } };
694
+ }
695
+ async function fetchGitHub(config, trigger, gh) {
696
+ const since = trigger.cursor ?? trigger.createdAt;
697
+ // A label trigger follows the moment the label is added (issue events), so an old labeled issue that merely gets a comment makes no card.
698
+ const labelEvents = config.watch === "issues" && config.label !== null;
699
+ const path = labelEvents ? `repos/${config.repo}/issues/events?per_page=50`
700
+ : config.watch === "issues" ? `repos/${config.repo}/issues?state=open&sort=created&direction=desc&per_page=30`
701
+ : config.watch === "pulls" ? `repos/${config.repo}/pulls?state=open&sort=created&direction=desc&per_page=30`
702
+ : `repos/${config.repo}/actions/runs?branch=${encodeURIComponent(config.branch ?? "main")}&status=failure&per_page=20`;
703
+ let result;
704
+ try {
705
+ result = await gh("gh", ["api", "-H", "Accept: application/vnd.github+json", path], { timeoutMs: 20_000 });
706
+ }
707
+ catch {
708
+ return { ok: false, problem: "Couldn't run gh, GitHub's command-line tool. Install it and sign in with gh auth login." };
709
+ }
710
+ if (result.code !== 0) {
711
+ const line = `${result.stderr}\n${result.stdout}`.split("\n").map(one => one.trim()).find(one => one !== "") ?? "";
712
+ return { ok: false, problem: /404|not found/i.test(line) ? `GitHub can't find ${config.repo}, or your GitHub login can't see it.`
713
+ : /401|auth|login/i.test(line) ? "GitHub needs you to sign in again: run gh auth login." : `GitHub didn't answer: ${line.replace(/[^\x20-\x7e]/g, "").slice(0, 160)}` };
714
+ }
715
+ let body;
716
+ try {
717
+ body = JSON.parse(result.stdout);
718
+ }
719
+ catch {
720
+ return { ok: false, problem: "GitHub's answer couldn't be read." };
721
+ }
722
+ const created = Date.parse(trigger.createdAt);
723
+ if (config.watch === "checks") {
724
+ const runs = (Array.isArray(record(body)["workflow_runs"]) ? record(body)["workflow_runs"] : []).filter(one => Date.parse(text(record(one)["created_at"])) > created);
725
+ const items = runs.reverse().map(one => githubRunItem(config, one)).filter((one) => one !== null);
726
+ return { ok: true, items: unique(items), cursor: trigger.cursor };
727
+ }
728
+ const list = Array.isArray(body) ? body : [];
729
+ if (labelEvents) {
730
+ const labeled = list.filter(one => text(record(one)["event"]) === "labeled" && text(record(record(one)["label"])["name"]).toLowerCase() === config.label.toLowerCase()
731
+ && Date.parse(text(record(one)["created_at"])) > Date.parse(since)).reverse();
732
+ const items = labeled.map(one => { const item = githubItem(config, record(one)["issue"], "issue"); return item === null ? null : { ...item, at: text(record(one)["created_at"]) || null }; }).filter((one) => one !== null);
733
+ return { ok: true, items: unique(items), cursor: labeled.map(one => text(record(one)["created_at"])).at(-1) ?? trigger.cursor };
734
+ }
735
+ // New issues and pull requests: opened after the trigger was added, oldest first; the key keeps each to one card.
736
+ const what = config.watch === "pulls" ? "pull" : "issue";
737
+ const items = list.filter(one => Date.parse(text(record(one)["created_at"])) > created).reverse().map(one => githubItem(config, one, what)).filter((one) => one !== null);
738
+ return { ok: true, items, cursor: trigger.cursor };
739
+ }
740
+ const unique = (items) => items.filter((item, index) => items.findIndex(one => one.key === item.key) === index);
741
+ export const LINEAR_URL = "https://api.linear.app/graphql";
742
+ export const LINEAR_QUERY = "query FlowTrigger($filter: IssueFilter) { issues(filter: $filter, first: 50, orderBy: updatedAt) { nodes { identifier title description url updatedAt state { name } team { key } labels { nodes { name } } } } }";
743
+ /** A Linear issue as a card; null when it doesn't match the trigger. */
744
+ export function linearItem(config, raw) {
745
+ const issue = record(raw);
746
+ const identifier = text(issue["identifier"]);
747
+ if (identifier === "")
748
+ return null;
749
+ const labelNodes = Array.isArray(issue["labels"]) ? issue["labels"] : Array.isArray(record(issue["labels"])["nodes"]) ? record(issue["labels"])["nodes"] : [];
750
+ const labels = labelNodes.map(one => text(record(one)["name"]).toLowerCase());
751
+ if (config.team !== null && text(record(issue["team"])["key"]).toUpperCase() !== config.team)
752
+ return null;
753
+ if (config.state !== null && text(record(issue["state"])["name"]).toLowerCase() !== config.state.toLowerCase())
754
+ return null;
755
+ if (config.label !== null && !labels.includes(config.label.toLowerCase()))
756
+ return null;
757
+ return { key: `linear:${identifier}`, at: text(issue["updatedAt"]) || null, title: text(issue["title"]) || identifier,
758
+ description: `From Linear ${identifier}:\n\n${text(issue["description"])}`.trim().slice(0, 4000),
759
+ source: { kind: "linear", label: `Linear ${identifier}`, url: text(issue["url"]) || null } };
760
+ }
761
+ async function fetchLinear(config, trigger, io) {
762
+ const key = readLinearKey(io.dir);
763
+ if (key === null)
764
+ return { ok: false, problem: "Needs a Linear API key. Add it on this flow's Triggers panel." };
765
+ const since = trigger.cursor ?? trigger.createdAt;
766
+ const filter = { updatedAt: { gt: since } };
767
+ if (config.team !== null)
768
+ filter["team"] = { key: { eqIgnoreCase: config.team } };
769
+ if (config.state !== null)
770
+ filter["state"] = { name: { eqIgnoreCase: config.state } };
771
+ if (config.label !== null)
772
+ filter["labels"] = { some: { name: { eqIgnoreCase: config.label } } };
773
+ let response;
774
+ try {
775
+ response = await io.fetch(LINEAR_URL, { method: "POST", headers: { "content-type": "application/json", authorization: key }, body: JSON.stringify({ query: LINEAR_QUERY, variables: { filter } }), signal: AbortSignal.timeout(20_000) });
776
+ }
777
+ catch {
778
+ return { ok: false, problem: "Couldn't reach Linear." };
779
+ }
780
+ let body;
781
+ try {
782
+ body = record(await response.json());
783
+ }
784
+ catch {
785
+ return { ok: false, problem: `Linear answered ${response.status} and it couldn't be read.` };
786
+ }
787
+ const errors = Array.isArray(body["errors"]) ? body["errors"].map(one => text(record(one)["message"])) : [];
788
+ if (response.status === 401 || errors.some(one => /authenticat|api key/i.test(one)))
789
+ return { ok: false, problem: "Linear didn't accept the key. Add a new one on this flow's Triggers panel." };
790
+ if (!response.ok || errors.length > 0)
791
+ return { ok: false, problem: `Linear said: ${(errors[0] ?? `status ${response.status}`).slice(0, 160)}` };
792
+ const nodes = record(record(record(body["data"])["issues"]))["nodes"];
793
+ const issues = (Array.isArray(nodes) ? nodes : []).sort((a, b) => text(record(a)["updatedAt"]).localeCompare(text(record(b)["updatedAt"])));
794
+ const items = issues.map(one => linearItem(config, one)).filter((one) => one !== null);
795
+ return { ok: true, items, cursor: issues.map(one => text(record(one)["updatedAt"])).filter(one => one !== "").at(-1) ?? trigger.cursor };
796
+ }
797
+ const header = (delivery, name) => { const value = delivery.headers[name]; return Array.isArray(value) ? value[0] ?? "" : value ?? ""; };
798
+ function sameHex(expected, given) {
799
+ const a = Buffer.from(expected, "utf8"), b = Buffer.from(given, "utf8");
800
+ return a.length === b.length && timingSafeEqual(a, b);
801
+ }
802
+ /** A webhook delivery at a trigger's secret address: prove it, read it, make at most the cards it names. Nothing runs here. */
803
+ export function receiveFlowHook(store, token, delivery, dir, now) {
804
+ if (!/^[A-Za-z0-9_-]{20,64}$/.test(token))
805
+ return { status: 404, said: "No such address." };
806
+ const trigger = store.flowTriggerByHook(hookHash(token));
807
+ const config = trigger === null ? null : triggerConfigOf(trigger);
808
+ if (trigger === null || config === null || !takesDeliveries(config))
809
+ return { status: 404, said: "No such address." };
810
+ if (delivery.body.length > HOOK_BYTES)
811
+ return { status: 413, said: "Too large." };
812
+ const note = (said) => { store.updateFlowTrigger(trigger.id, { lastAt: now.toISOString(), lastOutcome: said }, now); };
813
+ let payload;
814
+ try {
815
+ payload = JSON.parse(delivery.body.toString("utf8"));
816
+ }
817
+ catch {
818
+ return { status: 400, said: "Send JSON." };
819
+ }
820
+ let items = [];
821
+ if (config.kind === "github") {
822
+ const secret = readHookSecret(dir, trigger.id);
823
+ const signature = header(delivery, "x-hub-signature-256");
824
+ if (secret === null || !sameHex(`sha256=${createHmac("sha256", secret).update(delivery.body).digest("hex")}`, signature))
825
+ return { status: 401, said: "Signature doesn't match." };
826
+ const event = header(delivery, "x-github-event"), body = record(payload), action = text(body["action"]);
827
+ const repository = text(record(body["repository"])["full_name"]);
828
+ if (event === "ping") {
829
+ note("GitHub connected.");
830
+ return { status: 200, said: "Connected." };
831
+ }
832
+ if (repository.toLowerCase() !== config.repo.toLowerCase())
833
+ return { status: 202, said: "Not this repository." };
834
+ // A label trigger takes the moment its label is added (or an issue opened with it); a plain one takes new issues.
835
+ const labeledNow = action === "labeled" && config.label !== null && text(record(body["label"])["name"]).toLowerCase() === config.label.toLowerCase();
836
+ const found = config.watch === "issues" && event === "issues" && (action === "opened" || labeledNow) ? githubItem(config, body["issue"], "issue")
837
+ : config.watch === "pulls" && event === "pull_request" && ["opened", "ready_for_review", "labeled", "reopened"].includes(action) ? githubItem(config, body["pull_request"], "pull")
838
+ : config.watch === "checks" && event === "workflow_run" && action === "completed" ? githubRunItem(config, body["workflow_run"]) : null;
839
+ items = found === null ? [] : [found];
840
+ }
841
+ else if (config.kind === "linear") {
842
+ const secret = readHookSecret(dir, trigger.id);
843
+ if (secret === null)
844
+ return { status: 401, said: "Paste Linear's signing secret on the flow's Triggers panel first." };
845
+ if (!sameHex(createHmac("sha256", secret).update(delivery.body).digest("hex"), header(delivery, "linear-signature")))
846
+ return { status: 401, said: "Signature doesn't match." };
847
+ const body = record(payload), sent = Number(body["webhookTimestamp"]);
848
+ if (!Number.isFinite(sent) || Math.abs(now.getTime() - sent) > 5 * 60_000)
849
+ return { status: 401, said: "Too old." };
850
+ if (text(body["type"]) !== "Issue" || !["create", "update"].includes(text(body["action"])))
851
+ return { status: 202, said: "Ignored." };
852
+ // A state trigger fires when the issue arrives in that state, not on every later edit there.
853
+ const moved = text(body["action"]) === "create" || record(body["updatedFrom"])["stateId"] !== undefined || config.state === null;
854
+ const found = moved ? linearItem(config, body["data"]) : null;
855
+ items = found === null ? [] : [found];
856
+ }
857
+ else if (config.kind === "webhook") {
858
+ const pick = (path) => path === null ? null : path.split(".").reduce((node, part) => record(node)[part], payload);
859
+ const title = pick(config.titleField ?? "title") ?? pick("summary") ?? pick("message");
860
+ const bodyText = pick(config.bodyField ?? "description") ?? pick("body") ?? pick("text");
861
+ const id = header(delivery, "x-request-id") || header(delivery, "x-delivery-id") || header(delivery, "idempotency-key") || createHash("sha256").update(delivery.body).digest("hex");
862
+ items = [{ key: `hook:${id.slice(0, 120)}`, at: null, title: typeof title === "string" && title.trim() !== "" ? title : config.title,
863
+ description: `From a webhook:\n\n${typeof bodyText === "string" ? bodyText : JSON.stringify(payload, null, 2)}`.slice(0, 4000),
864
+ source: { kind: "webhook", label: "Webhook", url: null } }];
865
+ }
866
+ if (items.length === 0)
867
+ return { status: 202, said: "Nothing for this flow." };
868
+ const by = config.kind === "github" ? "GitHub" : config.kind === "linear" ? "Linear" : "Webhook";
869
+ const results = items.slice(0, CARDS_PER_CHECK).map(item => "skip" in item
870
+ ? (store.recordFlowTriggerEvent(trigger.id, item.key, null, item.skip, now) ? { made: "skipped", note: `${item.label}: ${item.skip}` } : { made: "seen", note: null })
871
+ : makeCard(store, trigger, config, item, by, now));
872
+ const said = outcomeWords(results, false);
873
+ note(said);
874
+ return { status: 202, said };
875
+ }
876
+ /** Save Linear's signing secret for a Linear webhook trigger (pasted on the secure panel, never in chat). */
877
+ export function saveLinearSigningSecret(trigger, secret, dir) {
878
+ const config = triggerConfigOf(trigger);
879
+ if (config?.kind !== "linear" || config.delivery !== "webhook")
880
+ return { ok: false, message: "That trigger doesn't take Linear webhooks." };
881
+ const clean = secret.trim();
882
+ if (!/^[A-Za-z0-9_-]{16,200}$/.test(clean))
883
+ return { ok: false, message: "Paste the signing secret exactly as Linear shows it." };
884
+ saveHookSecret(dir, trigger.id, clean);
885
+ return { ok: true };
886
+ }
887
+ /** Whether a webhook trigger can prove its deliveries: GitHub always (its secret is made here), Linear once its secret is pasted. */
888
+ export function hookReady(trigger, dir) {
889
+ const config = triggerConfigOf(trigger);
890
+ if (config === null || !takesDeliveries(config))
891
+ return false;
892
+ if (config.kind === "webhook")
893
+ return true;
894
+ return dir !== null && existsSync(secretFile(dir, trigger.id));
895
+ }
896
+ // ------------------------------------------------------ public forms (v84)
897
+ export const FORM_PATH = "/hooks/form/";
898
+ /** At most this many cards an hour from one shared form. */
899
+ const FORM_CARDS_PER_HOUR = 30;
900
+ /** Share a button as a secret link anyone can fill in, without signing in. The link is shown once; only its hash is kept. */
901
+ export function shareFlowButton(store, trigger, now, dir) {
902
+ if (triggerConfigOf(trigger)?.kind !== "button" || trigger.state === "removed")
903
+ return { ok: false, message: "Only a button can be shared as a form." };
904
+ const token = randomBytes(24).toString("base64url");
905
+ store.updateFlowTrigger(trigger.id, { hookHash: hookHash(token) }, now);
906
+ const base = readHooksBase(dir);
907
+ return { ok: true, id: trigger.id, said: "Form link made. Copy it now; it isn't shown again.", reveal: { path: `${FORM_PATH}${token}`, address: base === null ? null : `${base}${FORM_PATH}${token}`, secret: null } };
908
+ }
909
+ export function stopSharingFlowButton(store, trigger, now) {
910
+ store.updateFlowTrigger(trigger.id, { hookHash: null }, now);
911
+ }
912
+ const esc = (value) => value.replace(/[&<>"']/g, c => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" })[c]);
913
+ /** The public page: the button's name and its questions, nothing about the flow, the project or anyone in it. */
914
+ function formHtml(label, body) {
915
+ return `<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width, initial-scale=1"><meta name="robots" content="noindex">
916
+ <title>${esc(label)}</title><style>
917
+ :root{color-scheme:light dark;--bg:#f7f7f5;--card:#fff;--text:#1c1d1b;--muted:#666a63;--line:#dcded8;--accent:#2d5a45}
918
+ @media (prefers-color-scheme:dark){:root{--bg:#141614;--card:#1d201d;--text:#ecefe9;--muted:#9ea39a;--line:#343934;--accent:#8fd0ae}}
919
+ *{box-sizing:border-box}body{margin:0;background:var(--bg);color:var(--text);font:16px/1.5 system-ui,-apple-system,"Segoe UI",sans-serif}
920
+ main{max-width:560px;margin:0 auto;padding:40px 16px}h1{font-size:1.5rem;margin:0 0 20px}.card{background:var(--card);border:1px solid var(--line);border-radius:14px;padding:20px}
921
+ label{display:grid;gap:6px;margin:0 0 16px;font-weight:600}input,textarea{font:inherit;color:inherit;background:transparent;border:1px solid var(--line);border-radius:10px;padding:10px 12px;width:100%}
922
+ textarea{min-height:96px;resize:vertical}button{font:inherit;font-weight:600;border:0;border-radius:10px;padding:12px 18px;min-height:44px;background:var(--accent);color:var(--bg);cursor:pointer}
923
+ p{color:var(--muted)}.trap{position:absolute;left:-10000px;width:1px;height:1px;overflow:hidden}a{color:var(--accent)}
924
+ </style></head><body><main><h1>${esc(label)}</h1><div class="card">${body}</div></main></body></html>`;
925
+ }
926
+ /** GET a shared form: its questions. */
927
+ export function flowFormPage(store, token, now) {
928
+ const trigger = /^[A-Za-z0-9_-]{20,64}$/.test(token) ? store.flowTriggerByHook(hookHash(token)) : null;
929
+ const config = trigger === null ? null : triggerConfigOf(trigger);
930
+ if (trigger === null || config?.kind !== "button")
931
+ return { status: 404, html: formHtml("Not found", "<p>This form isn't available. Ask whoever shared it for a new link.</p>") };
932
+ const fields = config.questions.map((question, index) => `<label>${esc(question)}${index === 0 ? `<input name="a${index}" required maxlength="200" autocomplete="off">` : `<textarea name="a${index}" maxlength="3000"></textarea>`}</label>`).join("");
933
+ return { status: 200, html: formHtml(config.label, `<form method="post">${fields}<div class="trap" aria-hidden="true"><label>Leave this empty<input name="website" tabindex="-1" autocomplete="off"></label></div><input type="hidden" name="t" value="${now.getTime()}"><button>Send</button></form>`) };
934
+ }
935
+ /** POST a shared form: at most one card per submission, keys refused, a limit per hour, and quiet about what a bot did wrong. */
936
+ export function receiveFlowForm(store, token, fields, now) {
937
+ const trigger = /^[A-Za-z0-9_-]{20,64}$/.test(token) ? store.flowTriggerByHook(hookHash(token)) : null;
938
+ const config = trigger === null ? null : triggerConfigOf(trigger);
939
+ if (trigger === null || config?.kind !== "button")
940
+ return flowFormPage(store, token, now);
941
+ const again = `<p><a href="${esc(FORM_PATH + token)}">Send another</a></p>`;
942
+ const thanks = formHtml(config.label, `<p>Thanks — it's been sent.</p>${again}`);
943
+ // A filled trap, or a page posted faster than a person could, gets thanks and makes nothing.
944
+ const shown = Number(fields.get("t"));
945
+ if ((fields.get("website") ?? "") !== "" || !Number.isFinite(shown) || now.getTime() - shown < 2000 || now.getTime() - shown > 86_400_000)
946
+ return { status: 200, html: thanks };
947
+ const recent = store.handle.prepare("SELECT COUNT(*) AS n FROM flow_trigger_event WHERE trigger = ? AND key LIKE 'form:%' AND at > ?").get(trigger.id, new Date(now.getTime() - 3_600_000).toISOString());
948
+ if (Number(recent?.["n"] ?? 0) >= FORM_CARDS_PER_HOUR)
949
+ return { status: 429, html: formHtml(config.label, `<p>Too many sent in the last hour. Try again later.</p>`) };
950
+ const answers = config.questions.map((_question, index) => (fields.get(`a${index}`) ?? "").replace(/[\u0000-\u0008\u000b\u000c\u000e-\u001f\u007f]/g, "").trim().slice(0, index === 0 ? 200 : 3000));
951
+ if (answers[0] === "")
952
+ return { status: 400, html: formHtml(config.label, `<p>Answer “${esc(config.questions[0])}” first.</p>${again}`) };
953
+ const details = config.questions.slice(1).map((question, index) => answers[index + 1] === "" ? null : `${question}\n${answers[index + 1]}`).filter(one => one !== null).join("\n\n");
954
+ const made = makeCard(store, trigger, config, { key: `form:${now.toISOString()}:${randomUUID().slice(0, 8)}`, title: answers[0], description: `From the “${config.label}” form:\n\n${details}`.trim(), source: { kind: "form", label: `Form: ${config.label}`, url: null } }, "Form", now);
955
+ if (made.made !== "added")
956
+ return { status: 400, html: formHtml(config.label, `<p>${made.note?.includes("key or password") ? "That looks like it holds a key or password. Take it out and send again." : "That couldn't be sent."}</p>${again}`) };
957
+ store.updateFlowTrigger(trigger.id, { lastAt: now.toISOString(), lastOutcome: "Someone sent the form." }, now);
958
+ return { status: 200, html: thanks };
959
+ }