@agent-native/core 0.134.1 → 0.135.1

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 (463) hide show
  1. package/corpus/README.md +3 -3
  2. package/corpus/core/CHANGELOG.md +113 -0
  3. package/corpus/core/package.json +3 -2
  4. package/corpus/core/src/automations/service.ts +41 -4
  5. package/corpus/core/src/cli/mcp-config-writers.ts +27 -21
  6. package/corpus/core/src/client/agent-page/AgentJobsTab.tsx +187 -85
  7. package/corpus/core/src/client/agent-page/AutomationDetailsDialog.tsx +208 -0
  8. package/corpus/core/src/client/agent-page/AutomationScheduleDialog.tsx +180 -0
  9. package/corpus/core/src/client/agent-page/TimezoneSelect.tsx +241 -0
  10. package/corpus/core/src/client/agent-page/use-jobs.ts +50 -10
  11. package/corpus/core/src/client/org/OrgSwitcher.tsx +26 -0
  12. package/corpus/core/src/client/settings/AccountSettingsCard.tsx +35 -31
  13. package/corpus/core/src/client/settings/SchedulingTimezoneField.tsx +71 -0
  14. package/corpus/core/src/client/settings/SettingsRow.tsx +113 -0
  15. package/corpus/core/src/client/settings/agent-settings-search.ts +2 -1
  16. package/corpus/core/src/client/settings/index.ts +6 -0
  17. package/corpus/core/src/integrations/identity.ts +2 -12
  18. package/corpus/core/src/jobs/actions/list-automation-runs.ts +33 -0
  19. package/corpus/core/src/jobs/actions/list-recurring-jobs.ts +27 -6
  20. package/corpus/core/src/jobs/actions/manage-recurring-job.ts +41 -9
  21. package/corpus/core/src/jobs/background-automation-runner.ts +88 -0
  22. package/corpus/core/src/jobs/cron.ts +52 -6
  23. package/corpus/core/src/jobs/frontmatter.ts +20 -0
  24. package/corpus/core/src/jobs/run-history.ts +242 -0
  25. package/corpus/core/src/jobs/scheduler.ts +73 -18
  26. package/corpus/core/src/jobs/tools.ts +54 -6
  27. package/corpus/core/src/localization/actions/get-localization-preference.ts +2 -2
  28. package/corpus/core/src/localization/actions/set-localization-preference.ts +49 -11
  29. package/corpus/core/src/localization/default-messages.ts +27 -0
  30. package/corpus/core/src/localization/shared.ts +39 -6
  31. package/corpus/core/src/localization/user-timezone.ts +36 -0
  32. package/corpus/core/src/org/app-roles-handlers.ts +1 -9
  33. package/corpus/core/src/org/index.ts +2 -0
  34. package/corpus/core/src/org/membership.ts +18 -0
  35. package/corpus/core/src/review/actions/create-review-comment.ts +4 -1
  36. package/corpus/core/src/review/actions/reply-review-comment.ts +4 -1
  37. package/corpus/core/src/review/index.ts +5 -0
  38. package/corpus/core/src/review/notifications.ts +163 -0
  39. package/corpus/core/src/review/types.ts +7 -0
  40. package/corpus/core/src/server/action-discovery.ts +4 -0
  41. package/corpus/core/src/server/activity-notifications.ts +174 -0
  42. package/corpus/core/src/server/app-name.ts +31 -0
  43. package/corpus/core/src/server/better-auth-instance.ts +4 -4
  44. package/corpus/core/src/server/email-templates.ts +70 -14
  45. package/corpus/core/src/server/email.ts +66 -3
  46. package/corpus/core/src/server/index.ts +10 -0
  47. package/corpus/core/src/sharing/access.ts +44 -2
  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-31-editor-timeline-now-shows-video-frames-behind-the-waveform-g.md +6 -0
  109. package/corpus/templates/clips/changelog/2026-07-31-email-notifications-now-go-out-when-someone-comments-on-or-r.md +6 -0
  110. package/corpus/templates/clips/changelog/2026-07-31-you-can-now-set-a-default-visibility-for-new-recordings-in-s.md +6 -0
  111. package/corpus/templates/clips/changelog/2026-08-01-email-notifications-for-comments-and-reactions-now-actually-.md +6 -0
  112. package/corpus/templates/clips/changelog/2026-08-03-the-desktop-app-now-opens-directly-beneath-its-tray-icon-on-.md +6 -0
  113. package/corpus/templates/clips/chrome-extension/package.json +1 -0
  114. package/corpus/templates/clips/chrome-extension/vitest.config.ts +9 -0
  115. package/corpus/templates/clips/desktop/package.json +1 -0
  116. package/corpus/templates/clips/desktop/src/app.tsx +17 -1
  117. package/corpus/templates/clips/desktop/src/lib/recorder.ts +331 -36
  118. package/corpus/templates/clips/desktop/src/lib/upload-recovery.ts +129 -0
  119. package/corpus/templates/clips/desktop/src-tauri/src/native_screen.rs +815 -15
  120. package/corpus/templates/clips/desktop/src-tauri/src/tray.rs +61 -11
  121. package/corpus/templates/clips/desktop/vitest.config.ts +9 -0
  122. package/corpus/templates/clips/server/db/schema.ts +17 -0
  123. package/corpus/templates/clips/server/jobs/media-verification.ts +4 -0
  124. package/corpus/templates/clips/server/lib/activity-notifications.ts +185 -0
  125. package/corpus/templates/clips/server/lib/recording-media-cleanup.ts +2 -0
  126. package/corpus/templates/clips/server/lib/recording-upload-state.ts +46 -9
  127. package/corpus/templates/clips/server/lib/recordings.ts +24 -0
  128. package/corpus/templates/clips/server/lib/resumable-session.ts +10 -4
  129. package/corpus/templates/clips/server/lib/transactional-email-templates.ts +109 -0
  130. package/corpus/templates/clips/server/lib/upload-interruption.ts +8 -0
  131. package/corpus/templates/clips/server/lib/upload-lease.ts +50 -4
  132. package/corpus/templates/clips/server/lib/video-filmstrip-sprite.ts +207 -0
  133. package/corpus/templates/clips/server/lib/video-remux.ts +13 -4
  134. package/corpus/templates/clips/server/plugins/db.ts +16 -0
  135. package/corpus/templates/clips/server/routes/api/_agent-native-background/post-finalize-worker.post.ts +4 -0
  136. package/corpus/templates/clips/server/routes/api/uploads/[recordingId]/abort.post.ts +123 -36
  137. package/corpus/templates/clips/server/routes/api/uploads/[recordingId]/chunk.post.ts +159 -29
  138. package/corpus/templates/clips/server/routes/api/uploads/[recordingId]/interrupt.post.ts +197 -0
  139. package/corpus/templates/clips/server/routes/api/uploads/[recordingId]/reset-chunks.post.ts +193 -39
  140. package/corpus/templates/clips/server/routes/api/uploads/[recordingId]/resume.get.ts +191 -12
  141. package/corpus/templates/clips/shared/clips-ai-prefs.ts +6 -0
  142. package/corpus/templates/clips/shared/feature-flags.ts +12 -0
  143. package/corpus/templates/clips/shared/recording-core.ts +4 -0
  144. package/corpus/templates/clips/vitest.config.ts +23 -19
  145. package/corpus/templates/content/actions/_database-utils.ts +113 -4
  146. package/corpus/templates/content/actions/add-comment.ts +15 -1
  147. package/corpus/templates/content/actions/get-document.ts +84 -3
  148. package/corpus/templates/content/actions/list-documents.ts +1 -0
  149. package/corpus/templates/content/app/components/editor/BuilderBodySyncingNotice.tsx +9 -2
  150. package/corpus/templates/content/app/components/editor/DocumentEditor.tsx +34 -15
  151. package/corpus/templates/content/app/components/editor/DocumentToolbar.tsx +55 -13
  152. package/corpus/templates/content/app/components/editor/body-hydration.ts +12 -6
  153. package/corpus/templates/content/app/components/editor/database/DatabaseView.tsx +2 -3
  154. package/corpus/templates/content/app/components/sidebar/DocumentSidebar.tsx +5 -12
  155. package/corpus/templates/content/app/components/ui/switch.tsx +1 -0
  156. package/corpus/templates/content/app/hooks/use-content-database.ts +16 -27
  157. package/corpus/templates/content/app/hooks/use-content-prefs.ts +55 -0
  158. package/corpus/templates/content/app/hooks/use-create-page.ts +3 -6
  159. package/corpus/templates/content/app/hooks/use-document-properties.ts +9 -16
  160. package/corpus/templates/content/app/hooks/use-document-versions.ts +3 -3
  161. package/corpus/templates/content/app/hooks/use-documents.ts +85 -29
  162. package/corpus/templates/content/app/hooks/use-notion.ts +15 -13
  163. package/corpus/templates/content/app/i18n-data.ts +72 -1
  164. package/corpus/templates/content/app/lib/document-query.ts +36 -0
  165. package/corpus/templates/content/app/routes/_app.settings.tsx +47 -18
  166. package/corpus/templates/content/changelog/2026-07-30-breadcrumb-menus-now-hide-internal-system-pages.md +6 -0
  167. package/corpus/templates/content/changelog/2026-08-01-get-an-email-when-someone-comments-on-replies-in-or-mentions.md +6 -0
  168. package/corpus/templates/content/changelog/2026-08-02-pages-opened-from-a-database-now-keep-that-database-s-fields.md +6 -0
  169. package/corpus/templates/content/server/lib/comment-notifications.ts +146 -0
  170. package/corpus/templates/content/server/lib/document-context.ts +11 -6
  171. package/corpus/templates/content/server/routes/_agent-native/content/user-prefs.get.ts +17 -0
  172. package/corpus/templates/content/server/routes/_agent-native/content/user-prefs.put.ts +36 -0
  173. package/corpus/templates/content/shared/api.ts +8 -0
  174. package/corpus/templates/content/shared/content-user-prefs.ts +11 -0
  175. package/corpus/templates/content/vitest.config.ts +20 -16
  176. package/corpus/templates/crm/app/routes/settings.tsx +14 -22
  177. package/corpus/templates/crm/vitest.config.ts +8 -4
  178. package/corpus/templates/design/app/routes/settings.tsx +14 -22
  179. package/corpus/templates/design/changelog/2026-08-01-review-comments-replies-and-mentions-now-send-an-email-to-th.md +6 -0
  180. package/corpus/templates/design/vitest.config.ts +18 -12
  181. package/corpus/templates/dispatch/app/routes/settings.tsx +26 -40
  182. package/corpus/templates/dispatch/vitest.config.ts +11 -7
  183. package/corpus/templates/factory/app/routes/settings.tsx +14 -22
  184. package/corpus/templates/factory/vitest.config.ts +9 -0
  185. package/corpus/templates/forms/app/routes/_app.settings.tsx +14 -22
  186. package/corpus/templates/forms/vitest.config.ts +27 -23
  187. package/corpus/templates/macros/app/routes/settings.tsx +14 -22
  188. package/corpus/templates/macros/vitest.config.ts +16 -12
  189. package/corpus/templates/mail/app/pages/SettingsPage.tsx +14 -16
  190. package/corpus/templates/mail/vitest.config.ts +16 -12
  191. package/corpus/templates/plan/app/routes/settings.tsx +30 -44
  192. package/corpus/templates/plan/vitest.config.ts +24 -20
  193. package/corpus/templates/slides/actions/add-slide-comment.ts +12 -1
  194. package/corpus/templates/slides/app/components/ui/switch.tsx +1 -0
  195. package/corpus/templates/slides/app/hooks/use-slides-prefs.ts +55 -0
  196. package/corpus/templates/slides/app/i18n/en-US.ts +4 -0
  197. package/corpus/templates/slides/app/routes/settings.tsx +45 -22
  198. package/corpus/templates/slides/changelog/2026-08-01-get-an-email-when-someone-comments-on-or-replies-in-your-dec.md +6 -0
  199. package/corpus/templates/slides/server/lib/comment-notifications.ts +182 -0
  200. package/corpus/templates/slides/server/routes/_agent-native/slides/user-prefs.get.ts +16 -0
  201. package/corpus/templates/slides/server/routes/_agent-native/slides/user-prefs.put.ts +36 -0
  202. package/corpus/templates/slides/shared/slides-user-prefs.ts +11 -0
  203. package/corpus/templates/slides/vitest.config.ts +17 -13
  204. package/corpus/templates/tasks/app/components/layout/Sidebar.tsx +2 -0
  205. package/corpus/templates/tasks/app/routes/settings.tsx +14 -22
  206. package/corpus/templates/tasks/vitest.config.ts +17 -13
  207. package/corpus/toolkit/CHANGELOG.md +9 -0
  208. package/corpus/toolkit/package.json +1 -1
  209. package/corpus/toolkit/vitest.config.ts +3 -0
  210. package/dist/automations/service.d.ts +2 -0
  211. package/dist/automations/service.d.ts.map +1 -1
  212. package/dist/automations/service.js +29 -4
  213. package/dist/automations/service.js.map +1 -1
  214. package/dist/cli/mcp-config-writers.d.ts.map +1 -1
  215. package/dist/cli/mcp-config-writers.js +24 -23
  216. package/dist/cli/mcp-config-writers.js.map +1 -1
  217. package/dist/client/agent-page/AgentJobsTab.d.ts.map +1 -1
  218. package/dist/client/agent-page/AgentJobsTab.js +86 -29
  219. package/dist/client/agent-page/AgentJobsTab.js.map +1 -1
  220. package/dist/client/agent-page/AutomationDetailsDialog.d.ts +21 -0
  221. package/dist/client/agent-page/AutomationDetailsDialog.d.ts.map +1 -0
  222. package/dist/client/agent-page/AutomationDetailsDialog.js +33 -0
  223. package/dist/client/agent-page/AutomationDetailsDialog.js.map +1 -0
  224. package/dist/client/agent-page/AutomationScheduleDialog.d.ts +15 -0
  225. package/dist/client/agent-page/AutomationScheduleDialog.d.ts.map +1 -0
  226. package/dist/client/agent-page/AutomationScheduleDialog.js +46 -0
  227. package/dist/client/agent-page/AutomationScheduleDialog.js.map +1 -0
  228. package/dist/client/agent-page/TimezoneSelect.d.ts +17 -0
  229. package/dist/client/agent-page/TimezoneSelect.d.ts.map +1 -0
  230. package/dist/client/agent-page/TimezoneSelect.js +107 -0
  231. package/dist/client/agent-page/TimezoneSelect.js.map +1 -0
  232. package/dist/client/agent-page/use-jobs.d.ts +20 -7
  233. package/dist/client/agent-page/use-jobs.d.ts.map +1 -1
  234. package/dist/client/agent-page/use-jobs.js +24 -4
  235. package/dist/client/agent-page/use-jobs.js.map +1 -1
  236. package/dist/client/org/OrgSwitcher.d.ts +8 -1
  237. package/dist/client/org/OrgSwitcher.d.ts.map +1 -1
  238. package/dist/client/org/OrgSwitcher.js +8 -2
  239. package/dist/client/org/OrgSwitcher.js.map +1 -1
  240. package/dist/client/settings/AccountSettingsCard.d.ts.map +1 -1
  241. package/dist/client/settings/AccountSettingsCard.js +10 -9
  242. package/dist/client/settings/AccountSettingsCard.js.map +1 -1
  243. package/dist/client/settings/SchedulingTimezoneField.d.ts +10 -0
  244. package/dist/client/settings/SchedulingTimezoneField.d.ts.map +1 -0
  245. package/dist/client/settings/SchedulingTimezoneField.js +30 -0
  246. package/dist/client/settings/SchedulingTimezoneField.js.map +1 -0
  247. package/dist/client/settings/SettingsRow.d.ts +36 -0
  248. package/dist/client/settings/SettingsRow.d.ts.map +1 -0
  249. package/dist/client/settings/SettingsRow.js +15 -0
  250. package/dist/client/settings/SettingsRow.js.map +1 -0
  251. package/dist/client/settings/agent-settings-search.d.ts.map +1 -1
  252. package/dist/client/settings/agent-settings-search.js +1 -1
  253. package/dist/client/settings/agent-settings-search.js.map +1 -1
  254. package/dist/client/settings/index.d.ts +1 -0
  255. package/dist/client/settings/index.d.ts.map +1 -1
  256. package/dist/client/settings/index.js +1 -0
  257. package/dist/client/settings/index.js.map +1 -1
  258. package/dist/file-upload/actions/upload-image.d.ts +1 -1
  259. package/dist/integrations/identity.d.ts.map +1 -1
  260. package/dist/integrations/identity.js +2 -11
  261. package/dist/integrations/identity.js.map +1 -1
  262. package/dist/jobs/actions/list-automation-runs.d.ts +8 -0
  263. package/dist/jobs/actions/list-automation-runs.d.ts.map +1 -0
  264. package/dist/jobs/actions/list-automation-runs.js +29 -0
  265. package/dist/jobs/actions/list-automation-runs.js.map +1 -0
  266. package/dist/jobs/actions/list-recurring-jobs.d.ts +2 -0
  267. package/dist/jobs/actions/list-recurring-jobs.d.ts.map +1 -1
  268. package/dist/jobs/actions/list-recurring-jobs.js +20 -7
  269. package/dist/jobs/actions/list-recurring-jobs.js.map +1 -1
  270. package/dist/jobs/actions/manage-recurring-job.d.ts +2 -0
  271. package/dist/jobs/actions/manage-recurring-job.js +30 -9
  272. package/dist/jobs/actions/manage-recurring-job.js.map +1 -1
  273. package/dist/jobs/background-automation-runner.d.ts.map +1 -1
  274. package/dist/jobs/background-automation-runner.js +60 -0
  275. package/dist/jobs/background-automation-runner.js.map +1 -1
  276. package/dist/jobs/cron.d.ts +16 -2
  277. package/dist/jobs/cron.d.ts.map +1 -1
  278. package/dist/jobs/cron.js +43 -6
  279. package/dist/jobs/cron.js.map +1 -1
  280. package/dist/jobs/frontmatter.d.ts +12 -0
  281. package/dist/jobs/frontmatter.d.ts.map +1 -1
  282. package/dist/jobs/frontmatter.js +8 -0
  283. package/dist/jobs/frontmatter.js.map +1 -1
  284. package/dist/jobs/run-history.d.ts +56 -0
  285. package/dist/jobs/run-history.d.ts.map +1 -0
  286. package/dist/jobs/run-history.js +169 -0
  287. package/dist/jobs/run-history.js.map +1 -0
  288. package/dist/jobs/scheduler.d.ts.map +1 -1
  289. package/dist/jobs/scheduler.js +51 -18
  290. package/dist/jobs/scheduler.js.map +1 -1
  291. package/dist/jobs/tools.d.ts.map +1 -1
  292. package/dist/jobs/tools.js +37 -6
  293. package/dist/jobs/tools.js.map +1 -1
  294. package/dist/localization/actions/get-localization-preference.d.ts +1 -2
  295. package/dist/localization/actions/get-localization-preference.d.ts.map +1 -1
  296. package/dist/localization/actions/get-localization-preference.js.map +1 -1
  297. package/dist/localization/actions/set-localization-preference.d.ts +3 -3
  298. package/dist/localization/actions/set-localization-preference.d.ts.map +1 -1
  299. package/dist/localization/actions/set-localization-preference.js +33 -8
  300. package/dist/localization/actions/set-localization-preference.js.map +1 -1
  301. package/dist/localization/default-messages.d.ts +25 -0
  302. package/dist/localization/default-messages.d.ts.map +1 -1
  303. package/dist/localization/default-messages.js +25 -0
  304. package/dist/localization/default-messages.js.map +1 -1
  305. package/dist/localization/shared.d.ts +16 -1
  306. package/dist/localization/shared.d.ts.map +1 -1
  307. package/dist/localization/shared.js +22 -4
  308. package/dist/localization/shared.js.map +1 -1
  309. package/dist/localization/user-timezone.d.ts +11 -0
  310. package/dist/localization/user-timezone.d.ts.map +1 -0
  311. package/dist/localization/user-timezone.js +31 -0
  312. package/dist/localization/user-timezone.js.map +1 -0
  313. package/dist/notifications/routes.d.ts +4 -4
  314. package/dist/observability/routes.d.ts +3 -3
  315. package/dist/org/app-roles-handlers.d.ts.map +1 -1
  316. package/dist/org/app-roles-handlers.js +1 -8
  317. package/dist/org/app-roles-handlers.js.map +1 -1
  318. package/dist/org/index.d.ts +1 -0
  319. package/dist/org/index.d.ts.map +1 -1
  320. package/dist/org/index.js +1 -0
  321. package/dist/org/index.js.map +1 -1
  322. package/dist/org/membership.d.ts +6 -0
  323. package/dist/org/membership.d.ts.map +1 -0
  324. package/dist/org/membership.js +16 -0
  325. package/dist/org/membership.js.map +1 -0
  326. package/dist/progress/routes.d.ts +1 -1
  327. package/dist/provider-api/actions/custom-provider-registration.d.ts +13 -13
  328. package/dist/provider-api/actions/provider-api.d.ts +8 -8
  329. package/dist/provider-api/corpus-jobs.d.ts +2 -2
  330. package/dist/review/actions/create-review-comment.d.ts +32 -1
  331. package/dist/review/actions/create-review-comment.d.ts.map +1 -1
  332. package/dist/review/actions/create-review-comment.js +3 -1
  333. package/dist/review/actions/create-review-comment.js.map +1 -1
  334. package/dist/review/actions/reply-review-comment.d.ts +32 -1
  335. package/dist/review/actions/reply-review-comment.d.ts.map +1 -1
  336. package/dist/review/actions/reply-review-comment.js +3 -1
  337. package/dist/review/actions/reply-review-comment.js.map +1 -1
  338. package/dist/review/index.d.ts +1 -0
  339. package/dist/review/index.d.ts.map +1 -1
  340. package/dist/review/index.js +1 -0
  341. package/dist/review/index.js.map +1 -1
  342. package/dist/review/notifications.d.ts +24 -0
  343. package/dist/review/notifications.d.ts.map +1 -0
  344. package/dist/review/notifications.js +123 -0
  345. package/dist/review/notifications.js.map +1 -0
  346. package/dist/review/types.d.ts +5 -0
  347. package/dist/review/types.d.ts.map +1 -1
  348. package/dist/review/types.js.map +1 -1
  349. package/dist/server/action-discovery.d.ts.map +1 -1
  350. package/dist/server/action-discovery.js +4 -0
  351. package/dist/server/action-discovery.js.map +1 -1
  352. package/dist/server/activity-notifications.d.ts +61 -0
  353. package/dist/server/activity-notifications.d.ts.map +1 -0
  354. package/dist/server/activity-notifications.js +103 -0
  355. package/dist/server/activity-notifications.js.map +1 -0
  356. package/dist/server/agent-engine-api-key-route.d.ts +1 -1
  357. package/dist/server/app-name.d.ts +18 -0
  358. package/dist/server/app-name.d.ts.map +1 -1
  359. package/dist/server/app-name.js +31 -0
  360. package/dist/server/app-name.js.map +1 -1
  361. package/dist/server/better-auth-instance.js +4 -4
  362. package/dist/server/better-auth-instance.js.map +1 -1
  363. package/dist/server/email-templates.d.ts +12 -0
  364. package/dist/server/email-templates.d.ts.map +1 -1
  365. package/dist/server/email-templates.js +57 -14
  366. package/dist/server/email-templates.js.map +1 -1
  367. package/dist/server/email.d.ts +12 -0
  368. package/dist/server/email.d.ts.map +1 -1
  369. package/dist/server/email.js +52 -5
  370. package/dist/server/email.js.map +1 -1
  371. package/dist/server/index.d.ts +1 -0
  372. package/dist/server/index.d.ts.map +1 -1
  373. package/dist/server/index.js +1 -0
  374. package/dist/server/index.js.map +1 -1
  375. package/dist/server/transcribe-voice.d.ts +1 -1
  376. package/dist/sharing/access.d.ts.map +1 -1
  377. package/dist/sharing/access.js +32 -2
  378. package/dist/sharing/access.js.map +1 -1
  379. package/dist/sharing/index.d.ts +1 -0
  380. package/dist/sharing/index.d.ts.map +1 -1
  381. package/dist/sharing/index.js +1 -0
  382. package/dist/sharing/index.js.map +1 -1
  383. package/dist/sharing/recipients.d.ts +31 -0
  384. package/dist/sharing/recipients.d.ts.map +1 -0
  385. package/dist/sharing/recipients.js +40 -0
  386. package/dist/sharing/recipients.js.map +1 -0
  387. package/dist/templates/chat/app/routes/settings.tsx +14 -22
  388. package/dist/templates/chat/vitest.config.ts +9 -0
  389. package/dist/triggers/actions/list-automations.d.ts +2 -0
  390. package/dist/triggers/actions/list-automations.d.ts.map +1 -1
  391. package/dist/triggers/actions/list-automations.js +19 -8
  392. package/dist/triggers/actions/list-automations.js.map +1 -1
  393. package/dist/triggers/actions/manage-automation.d.ts +6 -0
  394. package/dist/triggers/actions/manage-automation.js +12 -6
  395. package/dist/triggers/actions/manage-automation.js.map +1 -1
  396. package/dist/triggers/actions.d.ts.map +1 -1
  397. package/dist/triggers/actions.js +15 -4
  398. package/dist/triggers/actions.js.map +1 -1
  399. package/dist/triggers/dispatcher.d.ts.map +1 -1
  400. package/dist/triggers/dispatcher.js +19 -17
  401. package/dist/triggers/dispatcher.js.map +1 -1
  402. package/dist/triggers/routes.d.ts.map +1 -1
  403. package/dist/triggers/routes.js +6 -6
  404. package/dist/triggers/routes.js.map +1 -1
  405. package/dist/vitest-config.d.ts +10 -0
  406. package/dist/vitest-config.d.ts.map +1 -0
  407. package/dist/vitest-config.js +55 -0
  408. package/dist/vitest-config.js.map +1 -0
  409. package/package.json +5 -4
  410. package/src/automations/service.ts +41 -4
  411. package/src/cli/mcp-config-writers.ts +27 -21
  412. package/src/client/agent-page/AgentJobsTab.tsx +187 -85
  413. package/src/client/agent-page/AutomationDetailsDialog.tsx +208 -0
  414. package/src/client/agent-page/AutomationScheduleDialog.tsx +180 -0
  415. package/src/client/agent-page/TimezoneSelect.tsx +241 -0
  416. package/src/client/agent-page/use-jobs.ts +50 -10
  417. package/src/client/org/OrgSwitcher.tsx +26 -0
  418. package/src/client/settings/AccountSettingsCard.tsx +35 -31
  419. package/src/client/settings/SchedulingTimezoneField.tsx +71 -0
  420. package/src/client/settings/SettingsRow.tsx +113 -0
  421. package/src/client/settings/agent-settings-search.ts +2 -1
  422. package/src/client/settings/index.ts +6 -0
  423. package/src/integrations/identity.ts +2 -12
  424. package/src/jobs/actions/list-automation-runs.ts +33 -0
  425. package/src/jobs/actions/list-recurring-jobs.ts +27 -6
  426. package/src/jobs/actions/manage-recurring-job.ts +41 -9
  427. package/src/jobs/background-automation-runner.ts +88 -0
  428. package/src/jobs/cron.ts +52 -6
  429. package/src/jobs/frontmatter.ts +20 -0
  430. package/src/jobs/run-history.ts +242 -0
  431. package/src/jobs/scheduler.ts +73 -18
  432. package/src/jobs/tools.ts +54 -6
  433. package/src/localization/actions/get-localization-preference.ts +2 -2
  434. package/src/localization/actions/set-localization-preference.ts +49 -11
  435. package/src/localization/default-messages.ts +27 -0
  436. package/src/localization/shared.ts +39 -6
  437. package/src/localization/user-timezone.ts +36 -0
  438. package/src/org/app-roles-handlers.ts +1 -9
  439. package/src/org/index.ts +2 -0
  440. package/src/org/membership.ts +18 -0
  441. package/src/review/actions/create-review-comment.ts +4 -1
  442. package/src/review/actions/reply-review-comment.ts +4 -1
  443. package/src/review/index.ts +5 -0
  444. package/src/review/notifications.ts +163 -0
  445. package/src/review/types.ts +7 -0
  446. package/src/server/action-discovery.ts +4 -0
  447. package/src/server/activity-notifications.ts +174 -0
  448. package/src/server/app-name.ts +31 -0
  449. package/src/server/better-auth-instance.ts +4 -4
  450. package/src/server/email-templates.ts +70 -14
  451. package/src/server/email.ts +66 -3
  452. package/src/server/index.ts +10 -0
  453. package/src/sharing/access.ts +44 -2
  454. package/src/sharing/index.ts +5 -0
  455. package/src/sharing/recipients.ts +67 -0
  456. package/src/templates/chat/app/routes/settings.tsx +14 -22
  457. package/src/templates/chat/vitest.config.ts +9 -0
  458. package/src/triggers/actions/list-automations.ts +27 -8
  459. package/src/triggers/actions/manage-automation.ts +17 -6
  460. package/src/triggers/actions.ts +16 -4
  461. package/src/triggers/dispatcher.ts +44 -30
  462. package/src/triggers/routes.ts +19 -6
  463. package/src/vitest-config.ts +68 -0
@@ -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,
@@ -11,13 +11,22 @@
11
11
  * site — keeps the transactional look-and-feel consistent.
12
12
  */
13
13
 
14
- import { getAppName } from "./app-name.js";
14
+ import { getAppName, getAppSlug, getAppDescription } from "./app-name.js";
15
15
  import { renderEmail, emailStrong } from "./email-template.js";
16
16
 
17
+ /** Shared reply-to for the framework's transactional emails. */
18
+ export const AGENT_NATIVE_REPLY_TO = "agent-native@builder.io";
19
+
17
20
  export interface RenderedEmailMessage {
18
21
  subject: string;
19
22
  html: string;
20
23
  text: string;
24
+ /**
25
+ * Per-app sender branding, applied by `sendEmail` only on first-party
26
+ * agent-native.com deployments. Self-hosted deployments keep the sender and
27
+ * reply-to they configured via EMAIL_FROM.
28
+ */
29
+ appSender?: { name: string; slug: string; replyTo?: string };
21
30
  }
22
31
 
23
32
  /**
@@ -33,6 +42,17 @@ function resolveAppName(): string {
33
42
  return stripCrlf(getAppName() || "Agent Native");
34
43
  }
35
44
 
45
+ /**
46
+ * Recipient-facing brand for auth emails. Only a recognized first-party
47
+ * template is presented as "Agent-Native <App>"; a custom deployment keeps its
48
+ * own name so its users aren't told they signed up for an Agent Native app.
49
+ */
50
+ function resolveBrand(slug: string | undefined): string {
51
+ const appName = getAppName();
52
+ if (!appName) return "Agent Native";
53
+ return slug ? `Agent-Native ${stripCrlf(appName)}` : stripCrlf(appName);
54
+ }
55
+
36
56
  // ---------------------------------------------------------------------------
37
57
  // Organization invitation
38
58
  // ---------------------------------------------------------------------------
@@ -89,28 +109,58 @@ export interface RenderVerifySignupEmailArgs {
89
109
  verifyUrl: string;
90
110
  }
91
111
 
112
+ /**
113
+ * Customer-facing description overrides for the verification email body. The
114
+ * default descriptions come from the app-picker hints, which name competitors
115
+ * for internal positioning; these rewrites frame them as "replacement" for a
116
+ * customer-facing email without changing the picker copy.
117
+ */
118
+ const VERIFY_EMAIL_DESCRIPTIONS: Record<string, string> = {
119
+ calendar:
120
+ "Agent-native Google Calendar replacement — manage events, sync, and public booking",
121
+ content:
122
+ "Open-source Obsidian/Notion replacement for MDX — edit local docs with agent assistance",
123
+ slides:
124
+ "Agent-native Google Slides replacement — generate and edit React presentations",
125
+ analytics:
126
+ "Agent-native Amplitude/Mixpanel replacement — connect data sources, prompt for charts",
127
+ mail: "Agent-native Superhuman replacement — email client with keyboard shortcuts and AI triage",
128
+ };
129
+
92
130
  export function renderVerifySignupEmail(
93
131
  args: RenderVerifySignupEmailArgs,
94
132
  ): RenderedEmailMessage {
95
133
  const email = stripCrlf(args.email);
96
- const appName = resolveAppName();
134
+ const slug = getAppSlug();
135
+ const brand = resolveBrand(slug);
136
+ const description = slug
137
+ ? (VERIFY_EMAIL_DESCRIPTIONS[slug] ?? getAppDescription())
138
+ : undefined;
139
+
140
+ const paragraphs = [
141
+ `Thanks for signing up for ${emailStrong(brand)}. To finish creating your account, confirm that ${emailStrong(email)} is your email address.`,
142
+ ];
143
+ if (description) {
144
+ paragraphs.push(`${stripCrlf(description).replace(/\.\s*$/, "")}.`);
145
+ }
146
+ paragraphs.push(`This link expires in 1 hour.`);
97
147
 
98
148
  const { html, text } = renderEmail({
99
- brandName: appName,
100
- preheader: `Confirm ${email} to finish setting up your ${appName} account.`,
101
- heading: `Verify your email for ${appName}`,
102
- paragraphs: [
103
- `Thanks for signing up for ${emailStrong(appName)}. To finish creating your account, confirm that ${emailStrong(email)} is your email address.`,
104
- `This link expires in 1 hour.`,
105
- ],
149
+ brandName: brand,
150
+ preheader: `Confirm ${email} to finish setting up your ${brand} account.`,
151
+ heading: `Verify your email for ${brand}`,
152
+ paragraphs,
106
153
  cta: { label: "Verify email", url: args.verifyUrl },
107
154
  footer: `If you didn't sign up, you can safely ignore this email.`,
108
155
  });
109
156
 
110
157
  return {
111
- subject: `Verify your email for ${appName}`,
158
+ subject: `Verify your email for ${brand}`,
112
159
  html,
113
160
  text,
161
+ appSender: slug
162
+ ? { name: brand, slug, replyTo: AGENT_NATIVE_REPLY_TO }
163
+ : undefined,
114
164
  };
115
165
  }
116
166
 
@@ -129,12 +179,15 @@ export function renderResetPasswordEmail(
129
179
  args: RenderResetPasswordEmailArgs,
130
180
  ): RenderedEmailMessage {
131
181
  const email = stripCrlf(args.email);
132
- const appName = resolveAppName();
182
+ // Match the verification email branding so password resets are clearly tied
183
+ // to the specific app. No value pitch here — it's a security email.
184
+ const slug = getAppSlug();
185
+ const brand = resolveBrand(slug);
133
186
 
134
187
  const { html, text } = renderEmail({
135
- brandName: appName,
188
+ brandName: brand,
136
189
  preheader: `Reset the password for ${email}. This link expires in 1 hour.`,
137
- heading: `Reset your ${appName} password`,
190
+ heading: `Reset your ${brand} password`,
138
191
  paragraphs: [
139
192
  `Someone requested a password reset for ${emailStrong(email)}. Click the button below to choose a new password.`,
140
193
  `This link expires in 1 hour.`,
@@ -144,8 +197,11 @@ export function renderResetPasswordEmail(
144
197
  });
145
198
 
146
199
  return {
147
- subject: `Reset your ${appName} password`,
200
+ subject: `Reset your ${brand} password`,
148
201
  html,
149
202
  text,
203
+ appSender: slug
204
+ ? { name: brand, slug, replyTo: AGENT_NATIVE_REPLY_TO }
205
+ : undefined,
150
206
  };
151
207
  }
@@ -39,6 +39,14 @@ export interface SendEmailArgs {
39
39
  fromName?: string;
40
40
  cc?: string | string[];
41
41
  replyTo?: string;
42
+ /**
43
+ * Per-app branding for first-party agent-native.com deployments. Applied
44
+ * only when the configured EMAIL_FROM is already on agent-native.com, so a
45
+ * self-hosted deployment keeps its own verified sender and support mailbox
46
+ * instead of sending as an unverified address a provider would reject.
47
+ * An explicit `from` / `replyTo` always wins.
48
+ */
49
+ appSender?: { name: string; slug: string; replyTo?: string };
42
50
  inReplyTo?: string;
43
51
  references?: string;
44
52
  attachments?: EmailAttachment[];
@@ -150,6 +158,56 @@ function withDisplayName(from: string, name: string): string {
150
158
  return `"${safe}" <${address}>`;
151
159
  }
152
160
 
161
+ const AGENT_NATIVE_SENDER_DOMAIN = "agent-native.com";
162
+
163
+ /**
164
+ * Resolve the per-app sender address, but only for deployments whose
165
+ * configured sender is already on agent-native.com. Any other (or missing)
166
+ * EMAIL_FROM means we cannot prove the branded address is a verified sender,
167
+ * so the deployment's own configuration is left untouched.
168
+ */
169
+ let warnedAppSenderSuppressed = false;
170
+
171
+ /**
172
+ * Suppressing the branding is correct for a sender we cannot prove we own,
173
+ * but it must not be invisible, or an operator sees generic senders with
174
+ * nothing pointing at why.
175
+ *
176
+ * EMAIL_FROM resolves per user/org/workspace through the scoped secret store,
177
+ * so the resolved address is tenant data and must never reach shared logs, and
178
+ * anything keyed by it would grow without bound in a warm worker. The message
179
+ * therefore carries no tenant values, which also makes it identical for every
180
+ * suppressed config — so emitting it once per process loses nothing.
181
+ */
182
+ function warnAppSenderSuppressed(): void {
183
+ if (warnedAppSenderSuppressed) return;
184
+ warnedAppSenderSuppressed = true;
185
+ console.warn(
186
+ `[agent-native:email] Per-app sender branding is off because the ` +
187
+ `configured EMAIL_FROM is not on ${AGENT_NATIVE_SENDER_DOMAIN}. ` +
188
+ `Transactional email keeps the configured sender. Expected when self-hosting.`,
189
+ );
190
+ }
191
+
192
+ function resolveAppSender(
193
+ configuredFrom: string | undefined,
194
+ appSender: SendEmailArgs["appSender"],
195
+ ): { address: string; name: string; replyTo?: string } | undefined {
196
+ if (!appSender) return undefined;
197
+ const address = configuredFrom
198
+ ? parseSendGridFrom(configuredFrom).email.toLowerCase()
199
+ : undefined;
200
+ if (!address?.endsWith(`@${AGENT_NATIVE_SENDER_DOMAIN}`)) {
201
+ warnAppSenderSuppressed();
202
+ return undefined;
203
+ }
204
+ return {
205
+ address: `${appSender.slug}@${AGENT_NATIVE_SENDER_DOMAIN}`,
206
+ name: appSender.name,
207
+ replyTo: appSender.replyTo,
208
+ };
209
+ }
210
+
153
211
  async function sendEmailWithSignal(
154
212
  args: SendEmailArgs,
155
213
  signal?: AbortSignal,
@@ -157,7 +215,12 @@ async function sendEmailWithSignal(
157
215
  const config = await resolveEmailTransport();
158
216
  signal?.throwIfAborted();
159
217
  const provider = config.provider;
160
- const from = getFromAddress(config, args.from, args.fromName);
218
+ const branded = resolveAppSender(config.from, args.appSender);
219
+ const from =
220
+ branded && !args.from
221
+ ? withDisplayName(branded.address, args.fromName ?? branded.name)
222
+ : getFromAddress(config, args.from, args.fromName);
223
+ const replyTo = args.replyTo ?? branded?.replyTo;
161
224
  const attachments = resolveAttachments(args);
162
225
 
163
226
  if (provider === "resend") {
@@ -169,7 +232,7 @@ async function sendEmailWithSignal(
169
232
  text: args.text,
170
233
  };
171
234
  if (args.cc) payload.cc = Array.isArray(args.cc) ? args.cc : [args.cc];
172
- if (args.replyTo) payload.reply_to = args.replyTo;
235
+ if (replyTo) payload.reply_to = replyTo;
173
236
  if (attachments?.length) {
174
237
  payload.attachments = attachments.map((a) => ({
175
238
  filename: a.filename,
@@ -220,7 +283,7 @@ async function sendEmailWithSignal(
220
283
  { type: "text/html", value: args.html },
221
284
  ],
222
285
  };
223
- if (args.replyTo) sgPayload.reply_to = parseSendGridFrom(args.replyTo);
286
+ if (replyTo) sgPayload.reply_to = parseSendGridFrom(replyTo);
224
287
  const sgHeaders: Record<string, string> = {};
225
288
  if (args.inReplyTo) sgHeaders["In-Reply-To"] = args.inReplyTo;
226
289
  if (args.references) sgHeaders["References"] = args.references;
@@ -547,6 +547,16 @@ export {
547
547
  type EmailProvider,
548
548
  type SendEmailArgs,
549
549
  } from "./email.js";
550
+ export {
551
+ notifyActivity,
552
+ runActivityNotification,
553
+ resolveActivityRecipients,
554
+ type ActivityDeliveryFailure,
555
+ type ActivityNotificationResult,
556
+ type ActivityNotificationStatus,
557
+ type NotifyActivityInput,
558
+ type ResolveActivityRecipientsInput,
559
+ } from "./activity-notifications.js";
550
560
  export {
551
561
  renderEmail,
552
562
  emailStrong,
@@ -15,6 +15,7 @@
15
15
 
16
16
  import { and, eq, or, sql, type SQL } from "drizzle-orm";
17
17
 
18
+ import { orgMembers } from "../org/schema.js";
18
19
  import {
19
20
  getRequestAuthCapability,
20
21
  getRequestUserEmail,
@@ -87,6 +88,35 @@ function emailColumnMatches(column: any, email: string): SQL {
87
88
  return sql`lower(${column}) = ${email}`;
88
89
  }
89
90
 
91
+ /**
92
+ * Real `org_members` membership, independent of the caller's currently
93
+ * active org (`ctx.orgId`). `org`-visibility access must key off actual
94
+ * membership — a user's active-org selection is a UI convenience, not a
95
+ * statement of which orgs they belong to.
96
+ *
97
+ * Queries through the resource's own `reg.getDb()` — the same connection
98
+ * every other lookup in this file uses — not the ambient `getDbExec()`,
99
+ * since `org_members` lives in that same app database.
100
+ */
101
+ async function isOrgMember(
102
+ reg: ShareableResourceRegistration,
103
+ memberOrgId: string,
104
+ email: string,
105
+ ): Promise<boolean> {
106
+ const db = reg.getDb() as any;
107
+ const rows = await db
108
+ .select({ id: orgMembers.id })
109
+ .from(orgMembers)
110
+ .where(
111
+ and(
112
+ eq(orgMembers.orgId, memberOrgId),
113
+ emailColumnMatches(orgMembers.email, email),
114
+ ),
115
+ )
116
+ .limit(1);
117
+ return rows.length > 0;
118
+ }
119
+
90
120
  /**
91
121
  * Build a Drizzle `WHERE` clause that admits rows the current user can see.
92
122
  * Pass the ownable resource table and its shares table; optional min role
@@ -470,7 +500,7 @@ async function resolveAccessImpl(
470
500
  const resource = await loadResourceForAccess(reg, resourceId, options);
471
501
  if (!resource) return null;
472
502
 
473
- const { userEmail, orgId } = ctx;
503
+ const { userEmail } = ctx;
474
504
  const normalizedUserEmail = normalizeEmailForAccess(userEmail);
475
505
 
476
506
  if (
@@ -491,7 +521,19 @@ async function resolveAccessImpl(
491
521
  // `visibility === "public"` on an `allowPublic: false` resource is treated
492
522
  // as private: only owner + explicit shares grant access. Falls through to
493
523
  // the explicit-share lookup below.
494
- if (resource.visibility === "org" && orgId && resource.orgId === orgId) {
524
+ //
525
+ // Membership in the resource's own org, not equality with the caller's
526
+ // currently active org: a caller can be a genuine member of the
527
+ // resource's org while a *different* org is their active selection, and
528
+ // `org` visibility should still admit them. Still requires some active
529
+ // org to be set at all (`orgId`), matching the pre-existing behavior for
530
+ // a caller with no active org.
531
+ if (
532
+ resource.visibility === "org" &&
533
+ resource.orgId &&
534
+ normalizedUserEmail &&
535
+ (await isOrgMember(reg, resource.orgId, normalizedUserEmail))
536
+ ) {
495
537
  const role = await highestShareRole(reg, resourceId, ctx, resource);
496
538
  return { role: role ?? "viewer", resource };
497
539
  }
@@ -34,3 +34,8 @@ export {
34
34
  type AccessContext,
35
35
  type ResolvedAccess,
36
36
  } from "./access.js";
37
+
38
+ export {
39
+ filterRecipientsByResourceAccess,
40
+ type FilterRecipientsInput,
41
+ } from "./recipients.js";
@@ -0,0 +1,67 @@
1
+ import { isOrgMember } from "../org/membership.js";
2
+ import { resolveAccess } from "./access.js";
3
+ import { roleSatisfies, type ShareRole } from "./schema.js";
4
+
5
+ export interface FilterRecipientsInput {
6
+ resourceType: string;
7
+ resourceId: string;
8
+ emails: Iterable<string>;
9
+ /** The resource's org, needed to honor `org` visibility. */
10
+ orgId?: string | null;
11
+ minimumRole?: ShareRole;
12
+ /**
13
+ * Override for resources that are not registered through `registerShareableResource`
14
+ * (the review registry resolves its own). Must honor `userEmail`/`orgId`
15
+ * rather than the ambient request, or the filter checks the wrong principal.
16
+ */
17
+ resolveRole?: (ctx: {
18
+ userEmail: string;
19
+ orgId?: string;
20
+ }) => Promise<{ role: string } | null>;
21
+ }
22
+
23
+ /**
24
+ * Keep only the addresses that can still open the resource *right now*.
25
+ *
26
+ * Notification recipient lists are built from historical rows — thread
27
+ * authors, stored mentions, caller-supplied addresses — none of which are an
28
+ * access grant. Without this, mentioning an arbitrary address mails it the
29
+ * comment body, and a collaborator whose share was revoked keeps receiving the
30
+ * thread. Access is re-resolved per address against the live ACL.
31
+ */
32
+ export async function filterRecipientsByResourceAccess({
33
+ resourceType,
34
+ resourceId,
35
+ emails,
36
+ orgId,
37
+ minimumRole = "viewer",
38
+ resolveRole,
39
+ }: FilterRecipientsInput): Promise<string[]> {
40
+ const resolve =
41
+ resolveRole ??
42
+ ((ctx: { userEmail: string; orgId?: string }) =>
43
+ resolveAccess(resourceType, resourceId, ctx));
44
+ const unique = new Set<string>();
45
+ for (const raw of emails) {
46
+ const email = raw.trim().toLowerCase();
47
+ if (email) unique.add(email);
48
+ }
49
+ if (unique.size === 0) return [];
50
+
51
+ const decisions = await Promise.all(
52
+ [...unique].map(async (email) => {
53
+ // Resolve without an org first: passing the resource's org up front
54
+ // would hand org-visibility access to anyone who is not in it.
55
+ const direct = await resolve({ userEmail: email });
56
+ if (direct && roleSatisfies(direct.role as ShareRole, minimumRole)) {
57
+ return email;
58
+ }
59
+ if (!orgId || !(await isOrgMember(orgId, email))) return null;
60
+ const viaOrg = await resolve({ userEmail: email, orgId });
61
+ return viaOrg && roleSatisfies(viaOrg.role as ShareRole, minimumRole)
62
+ ? email
63
+ : null;
64
+ }),
65
+ );
66
+ return decisions.filter((email): email is string => email !== null);
67
+ }