@agent-native/core 0.134.0 → 0.135.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 (493) hide show
  1. package/corpus/README.md +3 -3
  2. package/corpus/core/CHANGELOG.md +113 -0
  3. package/corpus/core/docs/content/template-content-developers.mdx +1 -1
  4. package/corpus/core/package.json +3 -2
  5. package/corpus/core/src/automations/service.ts +41 -4
  6. package/corpus/core/src/cli/mcp-config-writers.ts +27 -21
  7. package/corpus/core/src/client/agent-page/AgentJobsTab.tsx +187 -85
  8. package/corpus/core/src/client/agent-page/AutomationDetailsDialog.tsx +208 -0
  9. package/corpus/core/src/client/agent-page/AutomationScheduleDialog.tsx +180 -0
  10. package/corpus/core/src/client/agent-page/TimezoneSelect.tsx +241 -0
  11. package/corpus/core/src/client/agent-page/use-jobs.ts +50 -10
  12. package/corpus/core/src/client/org/OrgSwitcher.tsx +26 -0
  13. package/corpus/core/src/client/settings/AccountSettingsCard.tsx +35 -31
  14. package/corpus/core/src/client/settings/SchedulingTimezoneField.tsx +71 -0
  15. package/corpus/core/src/client/settings/SettingsRow.tsx +113 -0
  16. package/corpus/core/src/client/settings/agent-settings-search.ts +2 -1
  17. package/corpus/core/src/client/settings/index.ts +6 -0
  18. package/corpus/core/src/integrations/identity.ts +2 -12
  19. package/corpus/core/src/jobs/actions/list-automation-runs.ts +33 -0
  20. package/corpus/core/src/jobs/actions/list-recurring-jobs.ts +27 -6
  21. package/corpus/core/src/jobs/actions/manage-recurring-job.ts +41 -9
  22. package/corpus/core/src/jobs/background-automation-runner.ts +88 -0
  23. package/corpus/core/src/jobs/cron.ts +52 -6
  24. package/corpus/core/src/jobs/frontmatter.ts +20 -0
  25. package/corpus/core/src/jobs/run-history.ts +242 -0
  26. package/corpus/core/src/jobs/scheduler.ts +73 -18
  27. package/corpus/core/src/jobs/tools.ts +54 -6
  28. package/corpus/core/src/localization/actions/get-localization-preference.ts +2 -2
  29. package/corpus/core/src/localization/actions/set-localization-preference.ts +49 -11
  30. package/corpus/core/src/localization/default-messages.ts +27 -0
  31. package/corpus/core/src/localization/shared.ts +39 -6
  32. package/corpus/core/src/localization/user-timezone.ts +36 -0
  33. package/corpus/core/src/org/app-roles-handlers.ts +1 -9
  34. package/corpus/core/src/org/index.ts +2 -0
  35. package/corpus/core/src/org/membership.ts +18 -0
  36. package/corpus/core/src/review/actions/create-review-comment.ts +4 -1
  37. package/corpus/core/src/review/actions/reply-review-comment.ts +4 -1
  38. package/corpus/core/src/review/index.ts +5 -0
  39. package/corpus/core/src/review/notifications.ts +163 -0
  40. package/corpus/core/src/review/types.ts +7 -0
  41. package/corpus/core/src/server/action-discovery.ts +4 -0
  42. package/corpus/core/src/server/activity-notifications.ts +174 -0
  43. package/corpus/core/src/server/app-name.ts +31 -0
  44. package/corpus/core/src/server/better-auth-instance.ts +4 -4
  45. package/corpus/core/src/server/email-templates.ts +70 -14
  46. package/corpus/core/src/server/email.ts +66 -3
  47. package/corpus/core/src/server/index.ts +10 -0
  48. package/corpus/core/src/sharing/index.ts +5 -0
  49. package/corpus/core/src/sharing/recipients.ts +67 -0
  50. package/corpus/core/src/triggers/actions/list-automations.ts +27 -8
  51. package/corpus/core/src/triggers/actions/manage-automation.ts +17 -6
  52. package/corpus/core/src/triggers/actions.ts +16 -4
  53. package/corpus/core/src/triggers/dispatcher.ts +44 -30
  54. package/corpus/core/src/triggers/routes.ts +19 -6
  55. package/corpus/core/src/vitest-config.ts +68 -0
  56. package/corpus/core/vitest.config.ts +5 -0
  57. package/corpus/templates/analytics/app/pages/Settings.tsx +37 -53
  58. package/corpus/templates/analytics/changelog/2026-08-01-new-error-alerts-now-reach-you-by-email-and-a-scheduled-dash.md +6 -0
  59. package/corpus/templates/analytics/server/jobs/dashboard-report.ts +43 -0
  60. package/corpus/templates/analytics/server/lib/error-capture.ts +5 -1
  61. package/corpus/templates/analytics/vitest.config.ts +16 -12
  62. package/corpus/templates/assets/actions/refresh-generation-run.ts +3 -0
  63. package/corpus/templates/assets/app/components/ui/switch.tsx +1 -0
  64. package/corpus/templates/assets/app/hooks/use-assets-prefs.ts +55 -0
  65. package/corpus/templates/assets/app/i18n-data.ts +30 -0
  66. package/corpus/templates/assets/app/routes/settings.tsx +58 -55
  67. package/corpus/templates/assets/changelog/2026-08-01-get-an-email-when-a-generation-you-started-finishes-or-fails.md +6 -0
  68. package/corpus/templates/assets/server/lib/generation-run-notifications.ts +89 -0
  69. package/corpus/templates/assets/server/lib/video-runs.ts +8 -1
  70. package/corpus/templates/assets/server/routes/_agent-native/assets/user-prefs.get.ts +17 -0
  71. package/corpus/templates/assets/server/routes/_agent-native/assets/user-prefs.put.ts +36 -0
  72. package/corpus/templates/assets/shared/assets-user-prefs.ts +10 -0
  73. package/corpus/templates/assets/vitest.config.ts +16 -12
  74. package/corpus/templates/brain/app/routes/settings.tsx +15 -15
  75. package/corpus/templates/brain/vitest.config.ts +16 -12
  76. package/corpus/templates/calendar/app/pages/Settings.tsx +32 -40
  77. package/corpus/templates/calendar/changelog/2026-08-01-language-and-appearance-now-sit-together-in-one-preferences-.md +6 -0
  78. package/corpus/templates/calendar/vitest.config.ts +16 -12
  79. package/corpus/templates/chat/app/routes/settings.tsx +14 -22
  80. package/corpus/templates/chat/vitest.config.ts +9 -0
  81. package/corpus/templates/clips/.agents/skills/video-editing/SKILL.md +31 -0
  82. package/corpus/templates/clips/AGENTS.md +1 -0
  83. package/corpus/templates/clips/actions/add-comment.ts +12 -1
  84. package/corpus/templates/clips/actions/create-meeting.ts +2 -2
  85. package/corpus/templates/clips/actions/create-recording.ts +2 -2
  86. package/corpus/templates/clips/actions/finalize-recording.ts +42 -6
  87. package/corpus/templates/clips/actions/generate-filmstrip.ts +154 -0
  88. package/corpus/templates/clips/actions/get-recording-player-data.ts +6 -0
  89. package/corpus/templates/clips/actions/import-loom-recording.ts +2 -2
  90. package/corpus/templates/clips/actions/lib/ensure-recording-filmstrip.ts +258 -0
  91. package/corpus/templates/clips/actions/lib/ensure-seekable-video.ts +11 -3
  92. package/corpus/templates/clips/actions/react-to-comment.ts +13 -0
  93. package/corpus/templates/clips/actions/react-to-recording.ts +10 -1
  94. package/corpus/templates/clips/actions/reply-to-comment.ts +12 -1
  95. package/corpus/templates/clips/actions/stitch-recordings.ts +2 -2
  96. package/corpus/templates/clips/app/components/editor/editor-layout.tsx +152 -23
  97. package/corpus/templates/clips/app/components/editor/waveform.tsx +237 -75
  98. package/corpus/templates/clips/app/components/recorder/recorder-engine.ts +23 -1
  99. package/corpus/templates/clips/app/components/settings/ai-setup-section.tsx +441 -0
  100. package/corpus/templates/clips/app/components/settings/slack-section.tsx +329 -0
  101. package/corpus/templates/clips/app/components/settings/types.ts +14 -0
  102. package/corpus/templates/clips/app/components/settings/video-storage-section.tsx +443 -0
  103. package/corpus/templates/clips/app/hooks/use-secret-status.ts +69 -0
  104. package/corpus/templates/clips/app/i18n/en-US.ts +9 -1
  105. package/corpus/templates/clips/app/lib/video-filmstrip.ts +262 -0
  106. package/corpus/templates/clips/app/routes/_app.settings._index.tsx +165 -1351
  107. package/corpus/templates/clips/changelog/2026-07-27-saved-clips-now-resume-from-the-last-uploaded-byte-after-an-.md +6 -0
  108. package/corpus/templates/clips/changelog/2026-07-29-meeting-notes-no-longer-transcribe-the-other-side-twice-when.md +6 -0
  109. package/corpus/templates/clips/changelog/2026-07-31-editor-timeline-now-shows-video-frames-behind-the-waveform-g.md +6 -0
  110. package/corpus/templates/clips/changelog/2026-07-31-email-notifications-now-go-out-when-someone-comments-on-or-r.md +6 -0
  111. package/corpus/templates/clips/changelog/2026-07-31-you-can-now-set-a-default-visibility-for-new-recordings-in-s.md +6 -0
  112. package/corpus/templates/clips/changelog/2026-08-01-email-notifications-for-comments-and-reactions-now-actually-.md +6 -0
  113. package/corpus/templates/clips/changelog/2026-08-03-the-desktop-app-now-opens-directly-beneath-its-tray-icon-on-.md +6 -0
  114. package/corpus/templates/clips/chrome-extension/package.json +1 -0
  115. package/corpus/templates/clips/chrome-extension/vitest.config.ts +9 -0
  116. package/corpus/templates/clips/desktop/package.json +1 -0
  117. package/corpus/templates/clips/desktop/src/app.tsx +17 -1
  118. package/corpus/templates/clips/desktop/src/hooks/useMeetingTranscription.ts +37 -48
  119. package/corpus/templates/clips/desktop/src/lib/recorder.ts +331 -36
  120. package/corpus/templates/clips/desktop/src/lib/transcription-capture.ts +7 -5
  121. package/corpus/templates/clips/desktop/src/lib/transcription-engine.ts +182 -106
  122. package/corpus/templates/clips/desktop/src/lib/upload-recovery.ts +129 -0
  123. package/corpus/templates/clips/desktop/src/overlays/live-transcript.tsx +43 -14
  124. package/corpus/templates/clips/desktop/src/overlays/recording-pill.tsx +3 -3
  125. package/corpus/templates/clips/desktop/src-tauri/src/echo_guard.rs +443 -0
  126. package/corpus/templates/clips/desktop/src-tauri/src/lib.rs +1 -0
  127. package/corpus/templates/clips/desktop/src-tauri/src/native_screen.rs +815 -15
  128. package/corpus/templates/clips/desktop/src-tauri/src/tray.rs +61 -11
  129. package/corpus/templates/clips/desktop/src-tauri/src/whisper_speech.rs +125 -16
  130. package/corpus/templates/clips/desktop/vitest.config.ts +9 -0
  131. package/corpus/templates/clips/server/db/schema.ts +17 -0
  132. package/corpus/templates/clips/server/jobs/media-verification.ts +4 -0
  133. package/corpus/templates/clips/server/lib/activity-notifications.ts +185 -0
  134. package/corpus/templates/clips/server/lib/recording-media-cleanup.ts +2 -0
  135. package/corpus/templates/clips/server/lib/recording-upload-state.ts +46 -9
  136. package/corpus/templates/clips/server/lib/recordings.ts +24 -0
  137. package/corpus/templates/clips/server/lib/resumable-session.ts +10 -4
  138. package/corpus/templates/clips/server/lib/transactional-email-templates.ts +109 -0
  139. package/corpus/templates/clips/server/lib/upload-interruption.ts +8 -0
  140. package/corpus/templates/clips/server/lib/upload-lease.ts +50 -4
  141. package/corpus/templates/clips/server/lib/video-filmstrip-sprite.ts +207 -0
  142. package/corpus/templates/clips/server/lib/video-remux.ts +13 -4
  143. package/corpus/templates/clips/server/plugins/db.ts +16 -0
  144. package/corpus/templates/clips/server/routes/api/_agent-native-background/post-finalize-worker.post.ts +4 -0
  145. package/corpus/templates/clips/server/routes/api/uploads/[recordingId]/abort.post.ts +123 -36
  146. package/corpus/templates/clips/server/routes/api/uploads/[recordingId]/chunk.post.ts +159 -29
  147. package/corpus/templates/clips/server/routes/api/uploads/[recordingId]/interrupt.post.ts +197 -0
  148. package/corpus/templates/clips/server/routes/api/uploads/[recordingId]/reset-chunks.post.ts +193 -39
  149. package/corpus/templates/clips/server/routes/api/uploads/[recordingId]/resume.get.ts +191 -12
  150. package/corpus/templates/clips/shared/clips-ai-prefs.ts +6 -0
  151. package/corpus/templates/clips/shared/feature-flags.ts +12 -0
  152. package/corpus/templates/clips/shared/recording-core.ts +4 -0
  153. package/corpus/templates/clips/vitest.config.ts +23 -19
  154. package/corpus/templates/content/.agents/skills/document-editing/references/databases.md +6 -4
  155. package/corpus/templates/content/actions/_database-membership-lock.ts +25 -0
  156. package/corpus/templates/content/actions/_database-row-batch.ts +6 -2
  157. package/corpus/templates/content/actions/_database-source-utils.ts +289 -129
  158. package/corpus/templates/content/actions/_database-utils.ts +12 -3
  159. package/corpus/templates/content/actions/add-comment.ts +15 -1
  160. package/corpus/templates/content/actions/add-content-database-source-field-property.ts +39 -21
  161. package/corpus/templates/content/actions/attach-content-database-source.ts +4 -1
  162. package/corpus/templates/content/actions/change-content-database-source-role.ts +3 -5
  163. package/corpus/templates/content/actions/duplicate-document-property.ts +59 -46
  164. package/corpus/templates/content/actions/execute-builder-source-execution.ts +77 -71
  165. package/corpus/templates/content/actions/list-documents.ts +1 -0
  166. package/corpus/templates/content/actions/materialize-builder-required-fields.ts +7 -0
  167. package/corpus/templates/content/actions/remove-database-items.ts +183 -0
  168. package/corpus/templates/content/actions/set-document-property.ts +57 -39
  169. package/corpus/templates/content/actions/sync-local-folder-source.ts +7 -0
  170. package/corpus/templates/content/app/components/editor/DocumentEditor.tsx +5 -2
  171. package/corpus/templates/content/app/components/editor/DocumentToolbar.tsx +50 -1
  172. package/corpus/templates/content/app/components/editor/database/DatabaseView.tsx +471 -329
  173. package/corpus/templates/content/app/components/editor/database/row-access.ts +45 -0
  174. package/corpus/templates/content/app/components/editor/database/shared.tsx +141 -38
  175. package/corpus/templates/content/app/components/ui/switch.tsx +1 -0
  176. package/corpus/templates/content/app/hooks/use-content-database.ts +2 -2
  177. package/corpus/templates/content/app/hooks/use-content-prefs.ts +55 -0
  178. package/corpus/templates/content/app/i18n-data.ts +170 -0
  179. package/corpus/templates/content/app/routes/_app.settings.tsx +47 -18
  180. package/corpus/templates/content/changelog/2026-07-30-breadcrumb-menus-now-hide-internal-system-pages.md +6 -0
  181. package/corpus/templates/content/changelog/2026-07-30-database-rows-can-now-be-removed-without-deleting-their-page.md +6 -0
  182. package/corpus/templates/content/changelog/2026-08-01-get-an-email-when-someone-comments-on-replies-in-or-mentions.md +6 -0
  183. package/corpus/templates/content/parity/eval-scenarios.ts +2 -2
  184. package/corpus/templates/content/parity/matrix.md +1 -1
  185. package/corpus/templates/content/parity/matrix.ts +4 -5
  186. package/corpus/templates/content/server/lib/comment-notifications.ts +146 -0
  187. package/corpus/templates/content/server/routes/_agent-native/content/user-prefs.get.ts +17 -0
  188. package/corpus/templates/content/server/routes/_agent-native/content/user-prefs.put.ts +36 -0
  189. package/corpus/templates/content/shared/api.ts +4 -0
  190. package/corpus/templates/content/shared/content-user-prefs.ts +11 -0
  191. package/corpus/templates/content/vitest.config.ts +20 -16
  192. package/corpus/templates/crm/app/routes/settings.tsx +14 -22
  193. package/corpus/templates/crm/vitest.config.ts +8 -4
  194. package/corpus/templates/design/.generated/bridge/editor-chrome.generated.ts +26 -11
  195. package/corpus/templates/design/actions/take-design-screenshot.ts +7 -0
  196. package/corpus/templates/design/app/components/design/DesignCanvas.tsx +11 -3
  197. package/corpus/templates/design/app/components/design/MultiScreenCanvas.tsx +4 -1
  198. package/corpus/templates/design/app/components/design/bridge/editor-chrome.bridge.ts +54 -17
  199. package/corpus/templates/design/app/components/design/design-canvas/local-runtime.ts +126 -0
  200. package/corpus/templates/design/app/components/templates/TemplatePreview.tsx +2 -0
  201. package/corpus/templates/design/app/pages/Index.tsx +3 -1
  202. package/corpus/templates/design/app/pages/Present.tsx +2 -1
  203. package/corpus/templates/design/app/routes/settings.tsx +14 -22
  204. package/corpus/templates/design/changelog/2026-07-30-a-broken-inline-script-in-a-generated-screen-is-now-caught-o.md +6 -0
  205. package/corpus/templates/design/changelog/2026-07-30-design-html-is-now-validated-with-a-spec-grade-html-parser-i.md +6 -0
  206. package/corpus/templates/design/changelog/2026-07-30-designs-now-load-their-tailwind-and-alpine-runtimes-from-the.md +6 -0
  207. package/corpus/templates/design/changelog/2026-07-30-designs-with-a-broken-alpine-expression-are-now-caught-on-sa.md +6 -0
  208. package/corpus/templates/design/changelog/2026-07-30-editing-a-design-no-longer-strips-its-styling-the-canvas-kep.md +6 -0
  209. package/corpus/templates/design/changelog/2026-08-01-review-comments-replies-and-mentions-now-send-an-email-to-th.md +6 -0
  210. package/corpus/templates/design/package.json +4 -0
  211. package/corpus/templates/design/shared/html-integrity.ts +802 -556
  212. package/corpus/templates/design/vitest.config.ts +18 -12
  213. package/corpus/templates/dispatch/app/routes/settings.tsx +26 -40
  214. package/corpus/templates/dispatch/vitest.config.ts +11 -7
  215. package/corpus/templates/factory/app/routes/settings.tsx +14 -22
  216. package/corpus/templates/factory/vitest.config.ts +9 -0
  217. package/corpus/templates/forms/app/routes/_app.settings.tsx +14 -22
  218. package/corpus/templates/forms/vitest.config.ts +27 -23
  219. package/corpus/templates/macros/app/routes/settings.tsx +14 -22
  220. package/corpus/templates/macros/vitest.config.ts +16 -12
  221. package/corpus/templates/mail/app/pages/SettingsPage.tsx +14 -16
  222. package/corpus/templates/mail/vitest.config.ts +16 -12
  223. package/corpus/templates/plan/app/routes/settings.tsx +30 -44
  224. package/corpus/templates/plan/vitest.config.ts +24 -20
  225. package/corpus/templates/slides/actions/add-slide-comment.ts +12 -1
  226. package/corpus/templates/slides/app/components/ui/switch.tsx +1 -0
  227. package/corpus/templates/slides/app/hooks/use-slides-prefs.ts +55 -0
  228. package/corpus/templates/slides/app/i18n/en-US.ts +4 -0
  229. package/corpus/templates/slides/app/routes/settings.tsx +45 -22
  230. package/corpus/templates/slides/changelog/2026-08-01-get-an-email-when-someone-comments-on-or-replies-in-your-dec.md +6 -0
  231. package/corpus/templates/slides/server/lib/comment-notifications.ts +182 -0
  232. package/corpus/templates/slides/server/routes/_agent-native/slides/user-prefs.get.ts +16 -0
  233. package/corpus/templates/slides/server/routes/_agent-native/slides/user-prefs.put.ts +36 -0
  234. package/corpus/templates/slides/shared/slides-user-prefs.ts +11 -0
  235. package/corpus/templates/slides/vitest.config.ts +17 -13
  236. package/corpus/templates/tasks/app/components/layout/Sidebar.tsx +2 -0
  237. package/corpus/templates/tasks/app/routes/settings.tsx +14 -22
  238. package/corpus/templates/tasks/vitest.config.ts +17 -13
  239. package/corpus/toolkit/CHANGELOG.md +9 -0
  240. package/corpus/toolkit/package.json +1 -1
  241. package/corpus/toolkit/vitest.config.ts +3 -0
  242. package/dist/automations/service.d.ts +2 -0
  243. package/dist/automations/service.d.ts.map +1 -1
  244. package/dist/automations/service.js +29 -4
  245. package/dist/automations/service.js.map +1 -1
  246. package/dist/cli/mcp-config-writers.d.ts.map +1 -1
  247. package/dist/cli/mcp-config-writers.js +24 -23
  248. package/dist/cli/mcp-config-writers.js.map +1 -1
  249. package/dist/client/agent-page/AgentJobsTab.d.ts.map +1 -1
  250. package/dist/client/agent-page/AgentJobsTab.js +86 -29
  251. package/dist/client/agent-page/AgentJobsTab.js.map +1 -1
  252. package/dist/client/agent-page/AutomationDetailsDialog.d.ts +21 -0
  253. package/dist/client/agent-page/AutomationDetailsDialog.d.ts.map +1 -0
  254. package/dist/client/agent-page/AutomationDetailsDialog.js +33 -0
  255. package/dist/client/agent-page/AutomationDetailsDialog.js.map +1 -0
  256. package/dist/client/agent-page/AutomationScheduleDialog.d.ts +15 -0
  257. package/dist/client/agent-page/AutomationScheduleDialog.d.ts.map +1 -0
  258. package/dist/client/agent-page/AutomationScheduleDialog.js +46 -0
  259. package/dist/client/agent-page/AutomationScheduleDialog.js.map +1 -0
  260. package/dist/client/agent-page/TimezoneSelect.d.ts +17 -0
  261. package/dist/client/agent-page/TimezoneSelect.d.ts.map +1 -0
  262. package/dist/client/agent-page/TimezoneSelect.js +107 -0
  263. package/dist/client/agent-page/TimezoneSelect.js.map +1 -0
  264. package/dist/client/agent-page/use-jobs.d.ts +20 -7
  265. package/dist/client/agent-page/use-jobs.d.ts.map +1 -1
  266. package/dist/client/agent-page/use-jobs.js +24 -4
  267. package/dist/client/agent-page/use-jobs.js.map +1 -1
  268. package/dist/client/org/OrgSwitcher.d.ts +8 -1
  269. package/dist/client/org/OrgSwitcher.d.ts.map +1 -1
  270. package/dist/client/org/OrgSwitcher.js +8 -2
  271. package/dist/client/org/OrgSwitcher.js.map +1 -1
  272. package/dist/client/settings/AccountSettingsCard.d.ts.map +1 -1
  273. package/dist/client/settings/AccountSettingsCard.js +10 -9
  274. package/dist/client/settings/AccountSettingsCard.js.map +1 -1
  275. package/dist/client/settings/SchedulingTimezoneField.d.ts +10 -0
  276. package/dist/client/settings/SchedulingTimezoneField.d.ts.map +1 -0
  277. package/dist/client/settings/SchedulingTimezoneField.js +30 -0
  278. package/dist/client/settings/SchedulingTimezoneField.js.map +1 -0
  279. package/dist/client/settings/SettingsRow.d.ts +36 -0
  280. package/dist/client/settings/SettingsRow.d.ts.map +1 -0
  281. package/dist/client/settings/SettingsRow.js +15 -0
  282. package/dist/client/settings/SettingsRow.js.map +1 -0
  283. package/dist/client/settings/agent-settings-search.d.ts.map +1 -1
  284. package/dist/client/settings/agent-settings-search.js +1 -1
  285. package/dist/client/settings/agent-settings-search.js.map +1 -1
  286. package/dist/client/settings/index.d.ts +1 -0
  287. package/dist/client/settings/index.d.ts.map +1 -1
  288. package/dist/client/settings/index.js +1 -0
  289. package/dist/client/settings/index.js.map +1 -1
  290. package/dist/collab/struct-routes.d.ts +1 -1
  291. package/dist/file-upload/actions/upload-image.d.ts +1 -1
  292. package/dist/integrations/identity.d.ts.map +1 -1
  293. package/dist/integrations/identity.js +2 -11
  294. package/dist/integrations/identity.js.map +1 -1
  295. package/dist/jobs/actions/list-automation-runs.d.ts +8 -0
  296. package/dist/jobs/actions/list-automation-runs.d.ts.map +1 -0
  297. package/dist/jobs/actions/list-automation-runs.js +29 -0
  298. package/dist/jobs/actions/list-automation-runs.js.map +1 -0
  299. package/dist/jobs/actions/list-recurring-jobs.d.ts +2 -0
  300. package/dist/jobs/actions/list-recurring-jobs.d.ts.map +1 -1
  301. package/dist/jobs/actions/list-recurring-jobs.js +20 -7
  302. package/dist/jobs/actions/list-recurring-jobs.js.map +1 -1
  303. package/dist/jobs/actions/manage-recurring-job.d.ts +2 -0
  304. package/dist/jobs/actions/manage-recurring-job.js +30 -9
  305. package/dist/jobs/actions/manage-recurring-job.js.map +1 -1
  306. package/dist/jobs/background-automation-runner.d.ts.map +1 -1
  307. package/dist/jobs/background-automation-runner.js +60 -0
  308. package/dist/jobs/background-automation-runner.js.map +1 -1
  309. package/dist/jobs/cron.d.ts +16 -2
  310. package/dist/jobs/cron.d.ts.map +1 -1
  311. package/dist/jobs/cron.js +43 -6
  312. package/dist/jobs/cron.js.map +1 -1
  313. package/dist/jobs/frontmatter.d.ts +12 -0
  314. package/dist/jobs/frontmatter.d.ts.map +1 -1
  315. package/dist/jobs/frontmatter.js +8 -0
  316. package/dist/jobs/frontmatter.js.map +1 -1
  317. package/dist/jobs/run-history.d.ts +56 -0
  318. package/dist/jobs/run-history.d.ts.map +1 -0
  319. package/dist/jobs/run-history.js +169 -0
  320. package/dist/jobs/run-history.js.map +1 -0
  321. package/dist/jobs/scheduler.d.ts.map +1 -1
  322. package/dist/jobs/scheduler.js +51 -18
  323. package/dist/jobs/scheduler.js.map +1 -1
  324. package/dist/jobs/tools.d.ts.map +1 -1
  325. package/dist/jobs/tools.js +37 -6
  326. package/dist/jobs/tools.js.map +1 -1
  327. package/dist/localization/actions/get-localization-preference.d.ts +1 -2
  328. package/dist/localization/actions/get-localization-preference.d.ts.map +1 -1
  329. package/dist/localization/actions/get-localization-preference.js.map +1 -1
  330. package/dist/localization/actions/set-localization-preference.d.ts +3 -3
  331. package/dist/localization/actions/set-localization-preference.d.ts.map +1 -1
  332. package/dist/localization/actions/set-localization-preference.js +33 -8
  333. package/dist/localization/actions/set-localization-preference.js.map +1 -1
  334. package/dist/localization/default-messages.d.ts +25 -0
  335. package/dist/localization/default-messages.d.ts.map +1 -1
  336. package/dist/localization/default-messages.js +25 -0
  337. package/dist/localization/default-messages.js.map +1 -1
  338. package/dist/localization/shared.d.ts +16 -1
  339. package/dist/localization/shared.d.ts.map +1 -1
  340. package/dist/localization/shared.js +22 -4
  341. package/dist/localization/shared.js.map +1 -1
  342. package/dist/localization/user-timezone.d.ts +11 -0
  343. package/dist/localization/user-timezone.d.ts.map +1 -0
  344. package/dist/localization/user-timezone.js +31 -0
  345. package/dist/localization/user-timezone.js.map +1 -0
  346. package/dist/notifications/routes.d.ts +2 -2
  347. package/dist/observability/routes.d.ts +1 -1
  348. package/dist/org/app-roles-handlers.d.ts.map +1 -1
  349. package/dist/org/app-roles-handlers.js +1 -8
  350. package/dist/org/app-roles-handlers.js.map +1 -1
  351. package/dist/org/index.d.ts +1 -0
  352. package/dist/org/index.d.ts.map +1 -1
  353. package/dist/org/index.js +1 -0
  354. package/dist/org/index.js.map +1 -1
  355. package/dist/org/membership.d.ts +6 -0
  356. package/dist/org/membership.d.ts.map +1 -0
  357. package/dist/org/membership.js +16 -0
  358. package/dist/org/membership.js.map +1 -0
  359. package/dist/provider-api/actions/custom-provider-registration.d.ts +10 -10
  360. package/dist/provider-api/actions/provider-api.d.ts +8 -8
  361. package/dist/provider-api/corpus-jobs.d.ts +2 -2
  362. package/dist/resources/handlers.d.ts +1 -1
  363. package/dist/review/actions/create-review-comment.d.ts +32 -1
  364. package/dist/review/actions/create-review-comment.d.ts.map +1 -1
  365. package/dist/review/actions/create-review-comment.js +3 -1
  366. package/dist/review/actions/create-review-comment.js.map +1 -1
  367. package/dist/review/actions/reply-review-comment.d.ts +32 -1
  368. package/dist/review/actions/reply-review-comment.d.ts.map +1 -1
  369. package/dist/review/actions/reply-review-comment.js +3 -1
  370. package/dist/review/actions/reply-review-comment.js.map +1 -1
  371. package/dist/review/index.d.ts +1 -0
  372. package/dist/review/index.d.ts.map +1 -1
  373. package/dist/review/index.js +1 -0
  374. package/dist/review/index.js.map +1 -1
  375. package/dist/review/notifications.d.ts +24 -0
  376. package/dist/review/notifications.d.ts.map +1 -0
  377. package/dist/review/notifications.js +123 -0
  378. package/dist/review/notifications.js.map +1 -0
  379. package/dist/review/types.d.ts +5 -0
  380. package/dist/review/types.d.ts.map +1 -1
  381. package/dist/review/types.js.map +1 -1
  382. package/dist/server/action-discovery.d.ts.map +1 -1
  383. package/dist/server/action-discovery.js +4 -0
  384. package/dist/server/action-discovery.js.map +1 -1
  385. package/dist/server/activity-notifications.d.ts +61 -0
  386. package/dist/server/activity-notifications.d.ts.map +1 -0
  387. package/dist/server/activity-notifications.js +103 -0
  388. package/dist/server/activity-notifications.js.map +1 -0
  389. package/dist/server/agent-engine-api-key-route.d.ts +1 -1
  390. package/dist/server/app-name.d.ts +18 -0
  391. package/dist/server/app-name.d.ts.map +1 -1
  392. package/dist/server/app-name.js +31 -0
  393. package/dist/server/app-name.js.map +1 -1
  394. package/dist/server/better-auth-instance.js +4 -4
  395. package/dist/server/better-auth-instance.js.map +1 -1
  396. package/dist/server/email-templates.d.ts +12 -0
  397. package/dist/server/email-templates.d.ts.map +1 -1
  398. package/dist/server/email-templates.js +57 -14
  399. package/dist/server/email-templates.js.map +1 -1
  400. package/dist/server/email.d.ts +12 -0
  401. package/dist/server/email.d.ts.map +1 -1
  402. package/dist/server/email.js +52 -5
  403. package/dist/server/email.js.map +1 -1
  404. package/dist/server/index.d.ts +1 -0
  405. package/dist/server/index.d.ts.map +1 -1
  406. package/dist/server/index.js +1 -0
  407. package/dist/server/index.js.map +1 -1
  408. package/dist/sharing/index.d.ts +1 -0
  409. package/dist/sharing/index.d.ts.map +1 -1
  410. package/dist/sharing/index.js +1 -0
  411. package/dist/sharing/index.js.map +1 -1
  412. package/dist/sharing/recipients.d.ts +31 -0
  413. package/dist/sharing/recipients.d.ts.map +1 -0
  414. package/dist/sharing/recipients.js +40 -0
  415. package/dist/sharing/recipients.js.map +1 -0
  416. package/dist/templates/chat/app/routes/settings.tsx +14 -22
  417. package/dist/templates/chat/vitest.config.ts +9 -0
  418. package/dist/triggers/actions/list-automations.d.ts +2 -0
  419. package/dist/triggers/actions/list-automations.d.ts.map +1 -1
  420. package/dist/triggers/actions/list-automations.js +19 -8
  421. package/dist/triggers/actions/list-automations.js.map +1 -1
  422. package/dist/triggers/actions/manage-automation.d.ts +6 -0
  423. package/dist/triggers/actions/manage-automation.js +12 -6
  424. package/dist/triggers/actions/manage-automation.js.map +1 -1
  425. package/dist/triggers/actions.d.ts.map +1 -1
  426. package/dist/triggers/actions.js +15 -4
  427. package/dist/triggers/actions.js.map +1 -1
  428. package/dist/triggers/dispatcher.d.ts.map +1 -1
  429. package/dist/triggers/dispatcher.js +19 -17
  430. package/dist/triggers/dispatcher.js.map +1 -1
  431. package/dist/triggers/routes.d.ts.map +1 -1
  432. package/dist/triggers/routes.js +6 -6
  433. package/dist/triggers/routes.js.map +1 -1
  434. package/dist/vitest-config.d.ts +10 -0
  435. package/dist/vitest-config.d.ts.map +1 -0
  436. package/dist/vitest-config.js +55 -0
  437. package/dist/vitest-config.js.map +1 -0
  438. package/docs/content/template-content-developers.mdx +1 -1
  439. package/package.json +5 -4
  440. package/src/automations/service.ts +41 -4
  441. package/src/cli/mcp-config-writers.ts +27 -21
  442. package/src/client/agent-page/AgentJobsTab.tsx +187 -85
  443. package/src/client/agent-page/AutomationDetailsDialog.tsx +208 -0
  444. package/src/client/agent-page/AutomationScheduleDialog.tsx +180 -0
  445. package/src/client/agent-page/TimezoneSelect.tsx +241 -0
  446. package/src/client/agent-page/use-jobs.ts +50 -10
  447. package/src/client/org/OrgSwitcher.tsx +26 -0
  448. package/src/client/settings/AccountSettingsCard.tsx +35 -31
  449. package/src/client/settings/SchedulingTimezoneField.tsx +71 -0
  450. package/src/client/settings/SettingsRow.tsx +113 -0
  451. package/src/client/settings/agent-settings-search.ts +2 -1
  452. package/src/client/settings/index.ts +6 -0
  453. package/src/integrations/identity.ts +2 -12
  454. package/src/jobs/actions/list-automation-runs.ts +33 -0
  455. package/src/jobs/actions/list-recurring-jobs.ts +27 -6
  456. package/src/jobs/actions/manage-recurring-job.ts +41 -9
  457. package/src/jobs/background-automation-runner.ts +88 -0
  458. package/src/jobs/cron.ts +52 -6
  459. package/src/jobs/frontmatter.ts +20 -0
  460. package/src/jobs/run-history.ts +242 -0
  461. package/src/jobs/scheduler.ts +73 -18
  462. package/src/jobs/tools.ts +54 -6
  463. package/src/localization/actions/get-localization-preference.ts +2 -2
  464. package/src/localization/actions/set-localization-preference.ts +49 -11
  465. package/src/localization/default-messages.ts +27 -0
  466. package/src/localization/shared.ts +39 -6
  467. package/src/localization/user-timezone.ts +36 -0
  468. package/src/org/app-roles-handlers.ts +1 -9
  469. package/src/org/index.ts +2 -0
  470. package/src/org/membership.ts +18 -0
  471. package/src/review/actions/create-review-comment.ts +4 -1
  472. package/src/review/actions/reply-review-comment.ts +4 -1
  473. package/src/review/index.ts +5 -0
  474. package/src/review/notifications.ts +163 -0
  475. package/src/review/types.ts +7 -0
  476. package/src/server/action-discovery.ts +4 -0
  477. package/src/server/activity-notifications.ts +174 -0
  478. package/src/server/app-name.ts +31 -0
  479. package/src/server/better-auth-instance.ts +4 -4
  480. package/src/server/email-templates.ts +70 -14
  481. package/src/server/email.ts +66 -3
  482. package/src/server/index.ts +10 -0
  483. package/src/sharing/index.ts +5 -0
  484. package/src/sharing/recipients.ts +67 -0
  485. package/src/templates/chat/app/routes/settings.tsx +14 -22
  486. package/src/templates/chat/vitest.config.ts +9 -0
  487. package/src/triggers/actions/list-automations.ts +27 -8
  488. package/src/triggers/actions/manage-automation.ts +17 -6
  489. package/src/triggers/actions.ts +16 -4
  490. package/src/triggers/dispatcher.ts +44 -30
  491. package/src/triggers/routes.ts +19 -6
  492. package/src/vitest-config.ts +68 -0
  493. package/corpus/templates/content/actions/delete-database-items.ts +0 -89
@@ -24,8 +24,24 @@ export interface LocaleMetadata {
24
24
 
25
25
  export interface LocalizationPreference {
26
26
  locale: LocalePreference;
27
+ /**
28
+ * IANA zone used for the user's scheduled work. 'system' defers to the
29
+ * requesting browser, which is unavailable to headless callers (cron,
30
+ * chat integrations), so pinning a zone here is what makes a schedule
31
+ * mean the same thing no matter what created it.
32
+ *
33
+ * Optional because a record written before this field existed, or by a
34
+ * caller that only cares about language, is still a valid preference.
35
+ * Read it through normalizeLocalizationPreference to get a concrete value.
36
+ */
37
+ timezone?: TimezonePreference;
27
38
  }
28
39
 
40
+ /** A preference that has been through normalization: both fields resolved. */
41
+ export type ResolvedLocalizationPreference = Required<LocalizationPreference>;
42
+
43
+ export type TimezonePreference = "system" | (string & {});
44
+
29
45
  export const DEFAULT_LOCALE: LocaleCode = "en-US";
30
46
  export const LOCALIZATION_SETTING_KEY = "localization";
31
47
  export const LOCALE_STORAGE_KEY = "agent-native:locale-preference";
@@ -168,17 +184,34 @@ export function normalizeLocalePreference(
168
184
  return normalizeLocaleCode(value);
169
185
  }
170
186
 
187
+ export function normalizeTimezonePreference(
188
+ value: unknown,
189
+ ): TimezonePreference {
190
+ if (typeof value !== "string") return "system";
191
+ const trimmed = value.trim();
192
+ if (!trimmed || trimmed === "system") return "system";
193
+ try {
194
+ new Intl.DateTimeFormat("en-US", { timeZone: trimmed });
195
+ } catch {
196
+ return "system";
197
+ }
198
+ return trimmed;
199
+ }
200
+
171
201
  export function normalizeLocalizationPreference(
172
202
  value: unknown,
173
- ): LocalizationPreference {
203
+ ): ResolvedLocalizationPreference {
174
204
  if (value && typeof value === "object" && !Array.isArray(value)) {
175
- const locale = normalizeLocalePreference(
176
- (value as { locale?: unknown }).locale,
177
- );
178
- if (locale) return { locale };
205
+ const record = value as { locale?: unknown; timezone?: unknown };
206
+ // The two fields are independent: setting a timezone must not require
207
+ // also picking a language, which is the usual case.
208
+ return {
209
+ locale: normalizeLocalePreference(record.locale) ?? "system",
210
+ timezone: normalizeTimezonePreference(record.timezone),
211
+ };
179
212
  }
180
213
  const locale = normalizeLocalePreference(value);
181
- return { locale: locale ?? "system" };
214
+ return { locale: locale ?? "system", timezone: "system" };
182
215
  }
183
216
 
184
217
  export function localeDirection(locale: LocaleCode): "ltr" | "rtl" {
@@ -0,0 +1,36 @@
1
+ import { isValidTimezone, serverTimezone } from "../jobs/cron.js";
2
+ import { getRequestTimezone } from "../server/request-context.js";
3
+ import { getUserSetting } from "../settings/user-settings.js";
4
+ import {
5
+ LOCALIZATION_SETTING_KEY,
6
+ normalizeLocalizationPreference,
7
+ } from "./shared.js";
8
+
9
+ /**
10
+ * Resolve the IANA zone a user's scheduled work should be interpreted in.
11
+ *
12
+ * Order matters. A zone the user pinned in settings wins over the requesting
13
+ * browser: someone who set America/New_York expects an 8am job to stay 8am
14
+ * Eastern while they are travelling. The request header is only a fallback for
15
+ * users who never pinned one, and it is absent entirely for headless callers
16
+ * (cron, chat integrations, A2A) — which is why the stored preference exists.
17
+ */
18
+ export async function resolveUserSchedulingTimezone(
19
+ userEmail?: string | null,
20
+ ): Promise<string> {
21
+ if (userEmail) {
22
+ // Deliberately unguarded. The resolved zone is persisted into the
23
+ // schedule, so swallowing a read failure here would silently pin the job
24
+ // to the host zone and fire it at the wrong wall-clock time for its whole
25
+ // life. Failing the write is recoverable; a silently wrong schedule is not.
26
+ const preference = normalizeLocalizationPreference(
27
+ await getUserSetting(userEmail, LOCALIZATION_SETTING_KEY),
28
+ );
29
+ if (preference.timezone !== "system") return preference.timezone;
30
+ }
31
+ // Headers come from the client, so an unusable value must not reach
32
+ // frontmatter, where cron evaluation would quietly swap in the host zone.
33
+ const requested = getRequestTimezone();
34
+ if (requested && isValidTimezone(requested)) return requested;
35
+ return serverTimezone();
36
+ }
@@ -5,7 +5,6 @@ import {
5
5
  type H3Event,
6
6
  } from "h3";
7
7
 
8
- import { getDbExec } from "../db/client.js";
9
8
  import { readBody } from "../server/h3-helpers.js";
10
9
  import {
11
10
  getRegisteredAppRoles,
@@ -14,6 +13,7 @@ import {
14
13
  type AppRolesDescriptor,
15
14
  } from "./app-roles.js";
16
15
  import { getOrgContext } from "./context.js";
16
+ import { isOrgMember } from "./membership.js";
17
17
  import { canManageOrg } from "./permissions.js";
18
18
 
19
19
  /**
@@ -46,14 +46,6 @@ function extractMemberEmail(event: H3Event): string | undefined {
46
46
  return match?.[1] ? decodeURIComponent(match[1]) : undefined;
47
47
  }
48
48
 
49
- async function isOrgMember(orgId: string, email: string): Promise<boolean> {
50
- const { rows } = await getDbExec().execute({
51
- sql: `SELECT 1 FROM org_members WHERE org_id = ? AND LOWER(email) = ? LIMIT 1`,
52
- args: [orgId, email.trim().toLowerCase()],
53
- });
54
- return rows.length > 0;
55
- }
56
-
57
49
  /**
58
50
  * GET /_agent-native/org/app-roles?appId=X — the app's vocabulary plus every
59
51
  * assignment in the active org, and the caller's own resolved role.
@@ -92,3 +92,5 @@ export {
92
92
  } from "./app-roles-handlers.js";
93
93
 
94
94
  export { isFreeEmailProvider } from "./free-email-providers.js";
95
+
96
+ export { isOrgMember } from "./membership.js";
@@ -0,0 +1,18 @@
1
+ import { getDbExec } from "../db/client.js";
2
+
3
+ /**
4
+ * One resolver for "is this email in this org". Two private copies of this
5
+ * query already existed; a third would be the one that drifts.
6
+ */
7
+ export async function isOrgMember(
8
+ orgId: string,
9
+ email: string,
10
+ ): Promise<boolean> {
11
+ const normalized = email.trim().toLowerCase();
12
+ if (!orgId || !normalized) return false;
13
+ const { rows } = await getDbExec().execute({
14
+ sql: `SELECT 1 FROM org_members WHERE org_id = ? AND LOWER(email) = ? LIMIT 1`,
15
+ args: [orgId, normalized],
16
+ });
17
+ return rows.length > 0;
18
+ }
@@ -3,6 +3,7 @@ import { z } from "zod";
3
3
  import { defineAction } from "../../action.js";
4
4
  import { reviewAuthorNameFromContext } from "../identity.js";
5
5
  import { extractReviewMentions, normalizeReviewMentions } from "../mentions.js";
6
+ import { notifyReviewComment } from "../notifications.js";
6
7
  import {
7
8
  assertReviewableResourceAccess,
8
9
  normalizeReviewVisibility,
@@ -61,7 +62,7 @@ export default defineAction({
61
62
  const resolutionTarget =
62
63
  args.resolutionTarget ?? (mentions.length > 0 ? "human" : "agent");
63
64
 
64
- return insertReviewComment({
65
+ const comment = await insertReviewComment({
65
66
  resourceType: args.resourceType,
66
67
  resourceId: args.resourceId,
67
68
  targetId: args.targetId ?? null,
@@ -78,6 +79,8 @@ export default defineAction({
78
79
  visibility: normalizeReviewVisibility(access.visibility),
79
80
  metadata: args.metadata,
80
81
  });
82
+
83
+ return { ...comment, notified: await notifyReviewComment(comment) };
81
84
  },
82
85
  audit: {
83
86
  target: (args, result) => {
@@ -3,6 +3,7 @@ import { z } from "zod";
3
3
  import { defineAction } from "../../action.js";
4
4
  import { reviewAuthorNameFromContext } from "../identity.js";
5
5
  import { extractReviewMentions, normalizeReviewMentions } from "../mentions.js";
6
+ import { notifyReviewComment } from "../notifications.js";
6
7
  import {
7
8
  assertReviewableResourceAccess,
8
9
  normalizeReviewVisibility,
@@ -62,7 +63,7 @@ export default defineAction({
62
63
 
63
64
  const routeTarget =
64
65
  args.resolutionTarget ?? (mentions.length > 0 ? "human" : null);
65
- return insertReviewReply(
66
+ const reply = await insertReviewReply(
66
67
  {
67
68
  resourceType: args.resourceType,
68
69
  resourceId: args.resourceId,
@@ -88,6 +89,8 @@ export default defineAction({
88
89
  resourceId: args.resourceId,
89
90
  },
90
91
  );
92
+
93
+ return { ...reply, notified: await notifyReviewComment(reply) };
91
94
  },
92
95
  audit: {
93
96
  target: (args, result) => {
@@ -20,6 +20,11 @@ export {
20
20
  shouldRedactReviewIdentity,
21
21
  } from "./identity.js";
22
22
  export { extractReviewMentions, normalizeReviewMentions } from "./mentions.js";
23
+ export {
24
+ notifyReviewComment,
25
+ REVIEW_NOTIFICATION_PREFS_KEY,
26
+ type ReviewNotificationResult,
27
+ } from "./notifications.js";
23
28
  export {
24
29
  __resetReviewableResourcesForTests,
25
30
  assertReviewableResourceAccess,
@@ -0,0 +1,163 @@
1
+ /**
2
+ * Email notifications for review threads.
3
+ *
4
+ * Implemented here rather than in one app so every surface built on review
5
+ * comments (Design's review threads today) gets the same behavior. Recipient
6
+ * resolution, preference filtering, and delivery reporting come from the
7
+ * shared activity-notification helpers.
8
+ */
9
+
10
+ import {
11
+ notifyActivity,
12
+ runActivityNotification,
13
+ type ActivityNotificationResult,
14
+ } from "../server/activity-notifications.js";
15
+ import { getAppProductionUrl } from "../server/app-url.js";
16
+ import { emailStrong, renderEmail } from "../server/email-template.js";
17
+ import { sendEmail } from "../server/email.js";
18
+ import { filterRecipientsByResourceAccess } from "../sharing/recipients.js";
19
+ import {
20
+ getReviewableResource,
21
+ resolveReviewableResourceAccess,
22
+ } from "./registry.js";
23
+ import { queryReviewComments } from "./store.js";
24
+ import type { ReviewComment } from "./types.js";
25
+
26
+ /**
27
+ * Shared across every review surface. Apps that want a user-facing toggle
28
+ * write `{ emailNotifications: boolean }` under this key; an absent value
29
+ * means opted in.
30
+ */
31
+ export const REVIEW_NOTIFICATION_PREFS_KEY = "activity-notification-prefs";
32
+
33
+ const LOG_LABEL = "[review] comment notification";
34
+ const EXCERPT_LIMIT = 240;
35
+
36
+ function excerpt(body: string): string {
37
+ const collapsed = body.replace(/\s+/g, " ").trim();
38
+ return collapsed.length > EXCERPT_LIMIT
39
+ ? `${collapsed.slice(0, EXCERPT_LIMIT - 1)}…`
40
+ : collapsed;
41
+ }
42
+
43
+ async function resourceUrl(comment: ReviewComment): Promise<string> {
44
+ const registration = getReviewableResource(comment.resourceType);
45
+ const resolved = await registration?.resolveUrl?.(comment.resourceId);
46
+ return resolved?.trim() || getAppProductionUrl();
47
+ }
48
+
49
+ function resourceLabel(comment: ReviewComment): string {
50
+ return (
51
+ getReviewableResource(comment.resourceType)?.displayName?.trim() ||
52
+ comment.resourceType
53
+ );
54
+ }
55
+
56
+ async function threadParticipants(comment: ReviewComment): Promise<string[]> {
57
+ // Scope is already established by the action that inserted the comment;
58
+ // participants are read unscoped so a viewer's narrower scope cannot hide a
59
+ // person who is genuinely in the thread.
60
+ const comments = await queryReviewComments({
61
+ resourceType: comment.resourceType,
62
+ resourceId: comment.resourceId,
63
+ scope: { userEmail: null, orgId: null },
64
+ bypassScope: true,
65
+ includeResolved: true,
66
+ });
67
+ return comments
68
+ .filter((entry) => entry.threadId === comment.threadId)
69
+ .map((entry) => entry.authorEmail)
70
+ .filter((email): email is string => Boolean(email));
71
+ }
72
+
73
+ export type ReviewNotificationResult = ActivityNotificationResult;
74
+
75
+ /**
76
+ * Email the resource owner, mentioned people, and — on a reply — everyone else
77
+ * already in the thread. Never throws: the comment is already persisted, and a
78
+ * rejection here would make the client retry and duplicate it.
79
+ */
80
+ export async function notifyReviewComment(
81
+ comment: ReviewComment,
82
+ ): Promise<ReviewNotificationResult> {
83
+ return runActivityNotification(LOG_LABEL, () =>
84
+ deliverReviewCommentEmails(comment),
85
+ );
86
+ }
87
+
88
+ async function deliverReviewCommentEmails(
89
+ comment: ReviewComment,
90
+ ): Promise<ActivityNotificationResult> {
91
+ const mentioned = new Set(
92
+ comment.mentions
93
+ .map((mention) => mention.email?.trim().toLowerCase())
94
+ .filter((email): email is string => Boolean(email)),
95
+ );
96
+
97
+ const candidates = [comment.ownerEmail, ...mentioned];
98
+ const isReply = Boolean(comment.parentCommentId);
99
+ if (isReply) {
100
+ candidates.push(...(await threadParticipants(comment)));
101
+ }
102
+
103
+ // Mentions are caller-supplied and thread rows are historical; neither is an
104
+ // access grant. Re-check every address against the resource's current ACL
105
+ // before it can receive the comment body.
106
+ const allowed = await filterRecipientsByResourceAccess({
107
+ resourceType: comment.resourceType,
108
+ resourceId: comment.resourceId,
109
+ emails: candidates.filter((email): email is string => Boolean(email)),
110
+ orgId: comment.orgId,
111
+ // The review registry owns access for its types; unregistered ones resolve
112
+ // to null there, so nobody is notified rather than everybody.
113
+ resolveRole: (ctx) =>
114
+ resolveReviewableResourceAccess(
115
+ comment.resourceType,
116
+ comment.resourceId,
117
+ ctx,
118
+ ),
119
+ });
120
+
121
+ const actor = comment.authorName?.trim() || comment.authorEmail || "Someone";
122
+ const label = resourceLabel(comment);
123
+ const url = await resourceUrl(comment);
124
+
125
+ return notifyActivity({
126
+ candidates: allowed,
127
+ actorEmail: comment.authorEmail,
128
+ preferenceKey: REVIEW_NOTIFICATION_PREFS_KEY,
129
+ logLabel: LOG_LABEL,
130
+ send: async (to) => {
131
+ const wasMentioned = mentioned.has(to);
132
+ const lead = wasMentioned
133
+ ? `${emailStrong(actor)} mentioned you in a review comment on this ${label}.`
134
+ : isReply
135
+ ? `${emailStrong(actor)} replied in a review thread on this ${label}.`
136
+ : `${emailStrong(actor)} left a review comment on this ${label}.`;
137
+
138
+ const { html, text } = renderEmail({
139
+ preheader: `${actor} commented on this ${label}.`,
140
+ heading: wasMentioned
141
+ ? "You were mentioned in a review"
142
+ : isReply
143
+ ? "New reply in a review thread"
144
+ : "New review comment",
145
+ paragraphs: [lead, `"${excerpt(comment.body)}"`],
146
+ cta: { label: "Open review", url },
147
+ footer:
148
+ "You received this because you own, were mentioned in, or participated in this review thread.",
149
+ });
150
+
151
+ await sendEmail({
152
+ to,
153
+ subject: wasMentioned
154
+ ? `${actor} mentioned you in a review comment`
155
+ : isReply
156
+ ? `${actor} replied to a review thread`
157
+ : `${actor} left a review comment`,
158
+ html,
159
+ text,
160
+ });
161
+ },
162
+ });
163
+ }
@@ -36,6 +36,13 @@ export interface ReviewResourceContext {
36
36
  export interface ReviewableResourceRegistration {
37
37
  type: string;
38
38
  displayName?: string;
39
+ /**
40
+ * Deep link to the resource, used by review notification emails. Without it
41
+ * an email can only link to the app root.
42
+ */
43
+ resolveUrl?: (
44
+ resourceId: string,
45
+ ) => Promise<string | null | undefined> | string | null | undefined;
39
46
  resolveAccess?: (
40
47
  resourceId: string,
41
48
  ctx?: ReviewResourceContext,
@@ -625,6 +625,10 @@ export async function mergeCoreSharingActions(
625
625
  "manage-recurring-job",
626
626
  () => import("../jobs/actions/manage-recurring-job.js"),
627
627
  ],
628
+ [
629
+ "list-automation-runs",
630
+ () => import("../jobs/actions/list-automation-runs.js"),
631
+ ],
628
632
  [
629
633
  "list-automations",
630
634
  () => import("../triggers/actions/list-automations.js"),
@@ -0,0 +1,174 @@
1
+ /**
2
+ * Shared plumbing for collaboration emails (comments, replies, reactions,
3
+ * mentions).
4
+ *
5
+ * Two things every app was about to reimplement live here: resolving who
6
+ * should receive an activity email, and reporting delivery honestly. A
7
+ * workspace with no email provider is a *different* outcome from "nobody
8
+ * wanted this email" — collapsing both into an empty success is what let a
9
+ * dead notification toggle ship unnoticed.
10
+ */
11
+
12
+ import { getUserSetting } from "../settings/user-settings.js";
13
+ import { isEmailConfigured } from "./email.js";
14
+
15
+ export type ActivityDeliveryFailure = { email: string; error: string };
16
+
17
+ export type ActivityNotificationStatus =
18
+ | "delivered"
19
+ | "delivery-failed"
20
+ | "email-not-configured"
21
+ | "no-recipients"
22
+ | "notification-error";
23
+
24
+ export type ActivityNotificationResult = {
25
+ status: ActivityNotificationStatus;
26
+ sent: string[];
27
+ failed: ActivityDeliveryFailure[];
28
+ /** Set only on `notification-error`: why recipients could not be resolved. */
29
+ error?: string;
30
+ };
31
+
32
+ /** Default field read from an app's per-user preference blob. */
33
+ const DEFAULT_PREFERENCE_FIELD = "emailNotifications";
34
+
35
+ export interface ResolveActivityRecipientsInput {
36
+ /** Owner, thread participants, mentions — duplicates and blanks are fine. */
37
+ candidates: (string | null | undefined)[];
38
+ /** The person who caused the activity. Never emailed about their own action. */
39
+ actorEmail?: string | null;
40
+ /** App-owned user settings key, e.g. `clips-user-prefs`. */
41
+ preferenceKey: string;
42
+ /** Field inside that blob. Absent or non-`false` means opted in. */
43
+ preferenceField?: string;
44
+ }
45
+
46
+ function normalizeEmail(value: string | null | undefined): string {
47
+ return (value ?? "").trim().toLowerCase();
48
+ }
49
+
50
+ async function wantsActivityEmail(
51
+ email: string,
52
+ preferenceKey: string,
53
+ preferenceField: string,
54
+ ): Promise<boolean> {
55
+ const prefs = await getUserSetting(email, preferenceKey);
56
+ // A user who never opened settings has no stored blob; treat that as opted
57
+ // in rather than silently dropping their notifications.
58
+ return prefs?.[preferenceField] !== false;
59
+ }
60
+
61
+ /**
62
+ * Normalize, dedupe, drop the actor, and filter by each recipient's stored
63
+ * preference. Order follows first appearance in `candidates`.
64
+ */
65
+ export async function resolveActivityRecipients({
66
+ candidates,
67
+ actorEmail,
68
+ preferenceKey,
69
+ preferenceField = DEFAULT_PREFERENCE_FIELD,
70
+ }: ResolveActivityRecipientsInput): Promise<string[]> {
71
+ const actor = normalizeEmail(actorEmail);
72
+ const unique = new Set<string>();
73
+ for (const candidate of candidates) {
74
+ const email = normalizeEmail(candidate);
75
+ if (!email || email === actor || !email.includes("@")) continue;
76
+ unique.add(email);
77
+ }
78
+
79
+ const decisions = await Promise.all(
80
+ [...unique].map(async (email) =>
81
+ (await wantsActivityEmail(email, preferenceKey, preferenceField))
82
+ ? email
83
+ : null,
84
+ ),
85
+ );
86
+ return decisions.filter((email): email is string => email !== null);
87
+ }
88
+
89
+ export interface NotifyActivityInput extends ResolveActivityRecipientsInput {
90
+ /** Sends one email. Throwing marks that recipient failed, not the batch. */
91
+ send: (to: string) => Promise<unknown>;
92
+ /** Prefix for the delivery-failure log line, e.g. `[slides]`. */
93
+ logLabel?: string;
94
+ }
95
+
96
+ /**
97
+ * Resolve recipients and deliver one email each. Returns a status that
98
+ * distinguishes "no email provider" from "nobody to email" from a real send.
99
+ */
100
+ export async function notifyActivity({
101
+ send,
102
+ logLabel,
103
+ ...resolve
104
+ }: NotifyActivityInput): Promise<ActivityNotificationResult> {
105
+ if (!(await isEmailConfigured())) {
106
+ return { status: "email-not-configured", sent: [], failed: [] };
107
+ }
108
+
109
+ const recipients = await resolveActivityRecipients(resolve);
110
+ if (recipients.length === 0) {
111
+ return { status: "no-recipients", sent: [], failed: [] };
112
+ }
113
+
114
+ const sent: string[] = [];
115
+ const failed: ActivityDeliveryFailure[] = [];
116
+ for (const to of recipients) {
117
+ try {
118
+ await send(to);
119
+ sent.push(to);
120
+ } catch (error) {
121
+ failed.push({
122
+ email: to,
123
+ error: error instanceof Error ? error.message : String(error),
124
+ });
125
+ }
126
+ }
127
+
128
+ if (failed.length > 0) {
129
+ console.error(
130
+ `${logLabel ?? "[activity-notifications]"} delivery failed for ${failed
131
+ .map((failure) => `${failure.email} (${failure.error})`)
132
+ .join(", ")}`,
133
+ );
134
+ }
135
+
136
+ // Every recipient failing is an outage, not a delivery. Callers that log or
137
+ // surface this must not see it as the same outcome as a successful send.
138
+ return {
139
+ status: sent.length === 0 ? "delivery-failed" : "delivered",
140
+ sent,
141
+ failed,
142
+ };
143
+ }
144
+
145
+ /**
146
+ * Run an activity notification without letting it fail the write that caused
147
+ * it. The comment/reaction is already persisted by the time notification runs;
148
+ * rejecting here makes the client roll back and retry, which duplicates the
149
+ * row. The error still surfaces — as a distinct `notification-error` status,
150
+ * never as a silent success.
151
+ */
152
+ export async function runActivityNotification<
153
+ T extends {
154
+ status: string;
155
+ sent: string[];
156
+ failed: ActivityDeliveryFailure[];
157
+ },
158
+ >(
159
+ logLabel: string,
160
+ resolve: () => Promise<T>,
161
+ ): Promise<T | ActivityNotificationResult> {
162
+ try {
163
+ return await resolve();
164
+ } catch (error) {
165
+ const message = error instanceof Error ? error.message : String(error);
166
+ console.error(`${logLabel} could not be resolved: ${message}`);
167
+ return {
168
+ status: "notification-error",
169
+ error: message,
170
+ sent: [],
171
+ failed: [],
172
+ };
173
+ }
174
+ }
@@ -51,3 +51,34 @@ export function getAppName(): string | undefined {
51
51
  cachedFromPkg = name ?? undefined;
52
52
  return name;
53
53
  }
54
+
55
+ /**
56
+ * Resolve the app's slug — the machine-readable identifier used for the
57
+ * per-app transactional email sender (e.g. `clips@agent-native.com`).
58
+ *
59
+ * Only a first-party template name matched by package.json qualifies. A slug
60
+ * derived from an arbitrary `APP_NAME` must not mint a mailbox on the
61
+ * agent-native.com domain: a custom app would then send as
62
+ * `<its-name>@agent-native.com` and route replies to the Builder mailbox.
63
+ * Returns `undefined` for anything unrecognized, which leaves the deployment's
64
+ * configured sender in place.
65
+ */
66
+ export function getAppSlug(): string | undefined {
67
+ const pkg = readPkg();
68
+ if (!pkg?.name) return undefined;
69
+ return TEMPLATES.find((t) => t.name === pkg.name)?.name;
70
+ }
71
+
72
+ /**
73
+ * One-line description of the app, used to tailor transactional email bodies
74
+ * so recipients recognize which app they signed up for. Sourced from the
75
+ * first-party template metadata; `undefined` for unknown apps.
76
+ */
77
+ export function getAppDescription(): string | undefined {
78
+ const pkg = readPkg();
79
+ if (pkg?.name) {
80
+ const tmpl = TEMPLATES.find((t) => t.name === pkg.name);
81
+ if (tmpl) return tmpl.hint;
82
+ }
83
+ return undefined;
84
+ }
@@ -933,11 +933,11 @@ async function createBetterAuthInstance(
933
933
  ""
934
934
  ).replace(/\/$/, "");
935
935
  const resetUrl = `${appUrl}${appBasePath}/_agent-native/auth/reset?token=${encodeURIComponent(token)}`;
936
- const { subject, html, text } = renderResetPasswordEmail({
936
+ const { subject, html, text, appSender } = renderResetPasswordEmail({
937
937
  email: user.email,
938
938
  resetUrl,
939
939
  });
940
- await sendEmail({ to: user.email, subject, html, text });
940
+ await sendEmail({ to: user.email, subject, html, text, appSender });
941
941
  },
942
942
  },
943
943
  emailVerification: {
@@ -960,11 +960,11 @@ async function createBetterAuthInstance(
960
960
  const verifyUrl = verifyBasePath
961
961
  ? url.replace(/(\/\/[^/]+)(\/)/, `$1${verifyBasePath}$2`)
962
962
  : url;
963
- const { subject, html, text } = renderVerifySignupEmail({
963
+ const { subject, html, text, appSender } = renderVerifySignupEmail({
964
964
  email: user.email,
965
965
  verifyUrl,
966
966
  });
967
- await sendEmail({ to: user.email, subject, html, text });
967
+ await sendEmail({ to: user.email, subject, html, text, appSender });
968
968
  },
969
969
  },
970
970
  socialProviders,