@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
package/src/jobs/tools.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import type { ActionEntry } from "../agent/production-agent.js";
2
2
  import { getDbExec } from "../db/client.js";
3
+ import { resolveUserSchedulingTimezone } from "../localization/user-timezone.js";
3
4
  import {
4
5
  resourcePut,
5
6
  resourceGetByPath,
@@ -14,7 +15,13 @@ import {
14
15
  getRequestOrgId,
15
16
  getIntegrationRequestContext,
16
17
  } from "../server/request-context.js";
17
- import { isValidCron, nextOccurrence, describeCron } from "./cron.js";
18
+ import {
19
+ isValidCron,
20
+ nextOccurrence,
21
+ describeCron,
22
+ effectiveTimezone,
23
+ isValidTimezone,
24
+ } from "./cron.js";
18
25
  import { classifyJobResource } from "./frontmatter.js";
19
26
  import {
20
27
  parseJobFrontmatter,
@@ -95,6 +102,7 @@ export async function authorizeJobMutation(
95
102
 
96
103
  async function runCreate(args: Record<string, any>): Promise<string> {
97
104
  const { name, schedule, instructions, scope, runAs, model } = args;
105
+ const requestedTimezone = args.timezone;
98
106
 
99
107
  if (!name || !schedule || !instructions) {
100
108
  return JSON.stringify({
@@ -118,13 +126,24 @@ async function runCreate(args: Record<string, any>): Promise<string> {
118
126
  const owner = scope === "personal" ? getOwner() : getSharedOwner();
119
127
  const path = `jobs/${name}.md`;
120
128
  const now = new Date();
121
- const next = nextOccurrence(schedule, now);
129
+ // A cron time with no zone silently means the host's zone, which is how an
130
+ // "8am" job ends up firing at 4am for the person who asked for it.
131
+ if (requestedTimezone && !isValidTimezone(requestedTimezone)) {
132
+ return JSON.stringify({
133
+ error: `Unknown timezone: "${requestedTimezone}". Use an IANA zone such as America/New_York.`,
134
+ });
135
+ }
136
+ const timezone =
137
+ requestedTimezone ||
138
+ (await resolveUserSchedulingTimezone(getRequestUserEmail()));
139
+ const next = nextOccurrence(schedule, now, timezone);
122
140
  const integration = getIntegrationRequestContext();
123
141
  const channelId = integration?.incoming.platformContext.channelId;
124
142
  const threadRef = integration?.incoming.threadRef;
125
143
 
126
144
  const meta: JobFrontmatter = {
127
145
  schedule,
146
+ timezone,
128
147
  enabled: true,
129
148
  createdBy: getOwner(),
130
149
  orgId: getRequestOrgId() || undefined,
@@ -155,7 +174,8 @@ async function runCreate(args: Record<string, any>): Promise<string> {
155
174
  name,
156
175
  path,
157
176
  schedule,
158
- scheduleDescription: describeCron(schedule),
177
+ timezone,
178
+ scheduleDescription: describeCron(schedule, timezone),
159
179
  nextRun: next.toISOString(),
160
180
  scope: scope || "shared",
161
181
  ...(mcpTools?.length ? { mcpTools } : {}),
@@ -187,7 +207,10 @@ async function runList(args: Record<string, any>): Promise<string> {
187
207
  path: r.path,
188
208
  scope: r.owner === sharedOwner ? "shared" : "personal",
189
209
  schedule: meta.schedule,
190
- scheduleDescription: meta.schedule ? describeCron(meta.schedule) : "",
210
+ timezone: effectiveTimezone(meta.timezone),
211
+ scheduleDescription: meta.schedule
212
+ ? describeCron(meta.schedule, effectiveTimezone(meta.timezone))
213
+ : "",
191
214
  enabled: meta.enabled,
192
215
  lastRun: meta.lastRun || null,
193
216
  lastStatus: meta.lastStatus || null,
@@ -248,7 +271,23 @@ async function runUpdate(args: Record<string, any>): Promise<string> {
248
271
  });
249
272
  }
250
273
  meta.schedule = schedule;
251
- meta.nextRun = nextOccurrence(schedule).toISOString();
274
+ }
275
+
276
+ if (args.timezone !== undefined) {
277
+ if (!isValidTimezone(args.timezone)) {
278
+ return JSON.stringify({
279
+ error: `Unknown timezone: "${args.timezone}".`,
280
+ });
281
+ }
282
+ meta.timezone = args.timezone;
283
+ }
284
+
285
+ if (schedule || args.timezone !== undefined) {
286
+ meta.nextRun = nextOccurrence(
287
+ meta.schedule,
288
+ undefined,
289
+ meta.timezone,
290
+ ).toISOString();
252
291
  }
253
292
 
254
293
  if (enabled !== undefined) {
@@ -281,7 +320,11 @@ async function runUpdate(args: Record<string, any>): Promise<string> {
281
320
  updated: true,
282
321
  name,
283
322
  schedule: meta.schedule,
284
- scheduleDescription: describeCron(meta.schedule),
323
+ timezone: effectiveTimezone(meta.timezone),
324
+ scheduleDescription: describeCron(
325
+ meta.schedule,
326
+ effectiveTimezone(meta.timezone),
327
+ ),
285
328
  enabled: meta.enabled,
286
329
  nextRun: meta.nextRun,
287
330
  mcpTools: meta.mcpTools || [],
@@ -347,6 +390,11 @@ For jobs that use a connected MCP, pass the exact tool names in mcpTools. This b
347
390
  description:
348
391
  "Job name (hyphen-case, e.g. 'daily-scorecard-check'). Required for create and update.",
349
392
  },
393
+ timezone: {
394
+ type: "string",
395
+ description:
396
+ "IANA timezone the schedule's clock time is read in, e.g. 'America/New_York'. Optional; defaults to the user's saved scheduling timezone, then the caller's browser zone. Always pass this when the user names a time of day, so '8am' means 8am where they are rather than on the server.",
397
+ },
350
398
  schedule: {
351
399
  type: "string",
352
400
  description:
@@ -5,7 +5,7 @@ import { getUserSetting } from "../../settings/user-settings.js";
5
5
  import {
6
6
  LOCALIZATION_SETTING_KEY,
7
7
  normalizeLocalizationPreference,
8
- type LocalizationPreference,
8
+ type ResolvedLocalizationPreference,
9
9
  } from "../shared.js";
10
10
 
11
11
  export default defineAction({
@@ -13,7 +13,7 @@ export default defineAction({
13
13
  "Get the current user's interface language preference. Returns { locale }, where locale is 'system' or a supported BCP-47 locale.",
14
14
  schema: z.object({}),
15
15
  http: { method: "GET" },
16
- run: async (_args, ctx): Promise<LocalizationPreference> => {
16
+ run: async (_args, ctx): Promise<ResolvedLocalizationPreference> => {
17
17
  if (!ctx?.userEmail) throw new Error("Not authenticated.");
18
18
  const stored = await getUserSetting(
19
19
  ctx.userEmail,
@@ -1,31 +1,69 @@
1
1
  import { z } from "zod";
2
2
 
3
3
  import { defineAction } from "../../action.js";
4
- import { putUserSetting } from "../../settings/user-settings.js";
4
+ import {
5
+ getUserSetting,
6
+ putUserSetting,
7
+ } from "../../settings/user-settings.js";
5
8
  import {
6
9
  LOCALIZATION_SETTING_KEY,
7
10
  SUPPORTED_LOCALES,
8
11
  normalizeLocalePreference,
9
- type LocalizationPreference,
12
+ normalizeLocalizationPreference,
13
+ normalizeTimezonePreference,
14
+ type ResolvedLocalizationPreference,
10
15
  } from "../shared.js";
11
16
 
12
17
  export default defineAction({
13
18
  description:
14
- "Set the current user's interface language preference to 'system' or a supported BCP-47 locale.",
19
+ "Set the current user's interface language and scheduling timezone. Locale is 'system' or a supported BCP-47 locale; timezone is 'system' or an IANA zone such as America/New_York.",
15
20
  schema: z.object({
16
21
  locale: z
17
22
  .string()
18
- .describe("Language preference: 'system' or a supported BCP-47 locale."),
23
+ .describe("Language preference: 'system' or a supported BCP-47 locale.")
24
+ .optional(),
25
+ timezone: z
26
+ .string()
27
+ .describe("Scheduling timezone: 'system' or an IANA zone name.")
28
+ .optional(),
19
29
  }),
20
- run: async (args, ctx): Promise<LocalizationPreference> => {
30
+ run: async (args, ctx): Promise<ResolvedLocalizationPreference> => {
21
31
  if (!ctx?.userEmail) throw new Error("Not authenticated.");
22
- const locale = normalizeLocalePreference(args.locale);
23
- if (!locale) {
24
- throw new Error(
25
- `Unsupported locale. Use system, ${SUPPORTED_LOCALES.join(", ")}.`,
26
- );
32
+
33
+ // Each field is optional, so merge onto what is stored instead of
34
+ // writing a partial record that silently resets the other one.
35
+ const current = normalizeLocalizationPreference(
36
+ await getUserSetting(ctx.userEmail, LOCALIZATION_SETTING_KEY),
37
+ );
38
+
39
+ let locale = current.locale;
40
+ if (args.locale !== undefined) {
41
+ const parsed = normalizeLocalePreference(args.locale);
42
+ if (!parsed) {
43
+ throw new Error(
44
+ `Unsupported locale. Use system, ${SUPPORTED_LOCALES.join(", ")}.`,
45
+ );
46
+ }
47
+ locale = parsed;
27
48
  }
28
- const value: LocalizationPreference & Record<string, unknown> = { locale };
49
+
50
+ let timezone = current.timezone;
51
+ if (args.timezone !== undefined) {
52
+ const parsed = normalizeTimezonePreference(args.timezone);
53
+ // Normalizing an unusable zone to "system" would quietly schedule the
54
+ // user in the host zone, which is the failure this field exists to fix.
55
+ if (parsed === "system" && args.timezone.trim() !== "system") {
56
+ throw new Error(
57
+ `Unknown timezone "${args.timezone}". Use system or an IANA zone such as America/New_York.`,
58
+ );
59
+ }
60
+ timezone = parsed;
61
+ }
62
+
63
+ const value: ResolvedLocalizationPreference & Record<string, unknown> = {
64
+ locale,
65
+ timezone,
66
+ };
29
67
  await putUserSetting(ctx.userEmail, LOCALIZATION_SETTING_KEY, value);
30
68
  return value;
31
69
  },
@@ -57,6 +57,10 @@ const messages = {
57
57
  profileSaved: "Profile updated",
58
58
  profileSaveError: "Could not update profile",
59
59
  profileMenuItem: "Profile",
60
+ manageAgentMenuItem: "Manage agent",
61
+ timezoneLabel: "Timezone",
62
+ timezoneSystem: "Follow this browser ({{zone}})",
63
+ timezoneHint: "Used for timestamps and scheduled automations.",
60
64
  },
61
65
  secrets: {
62
66
  scopeLabel: "Scope",
@@ -563,6 +567,7 @@ const messages = {
563
567
  systemOrdered: "System · ordered, not evictable",
564
568
  },
565
569
  jobs: {
570
+ timezone: "Timezone",
566
571
  pageTitle: "Automations",
567
572
  pageDescription:
568
573
  "Manage agent tasks that run on a schedule or in response to events.",
@@ -582,6 +587,28 @@ const messages = {
582
587
  paused: "Paused",
583
588
  nextRun: "Next run",
584
589
  lastRun: "Last run",
590
+ lastChecked: "Last checked",
591
+ lastStatus: "Last status",
592
+ neverRan: "Never",
593
+ notSet: "—",
594
+ status: "Status",
595
+ trigger: "Trigger",
596
+ scope: "Scope",
597
+ createdBy: "Created by",
598
+ model: "Model",
599
+ blockedTitle: "This automation is not running",
600
+ pastRuns: "Past runs",
601
+ runsLoadError: "Could not load run history.",
602
+ noRuns:
603
+ "This automation has not executed yet. Only real executions are recorded here.",
604
+ edit: "Edit",
605
+ editScheduleTitle: "Edit schedule — {{name}}",
606
+ editScheduleDescription:
607
+ "The clock time below is read in the timezone you pick, so 8:00 means 8:00 there.",
608
+ cronExpression: "Cron expression",
609
+ cronFormatHint: "minute hour day-of-month month day-of-week",
610
+ cronFieldCount: "A cron expression needs exactly 5 fields.",
611
+ saveSchedule: "Save schedule",
585
612
  details: "Details",
586
613
  pause: "Pause",
587
614
  resume: "Resume",
@@ -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.
package/src/org/index.ts CHANGED
@@ -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,