@agent-native/core 0.134.1 → 0.135.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/corpus/README.md +3 -3
- package/corpus/core/CHANGELOG.md +107 -0
- package/corpus/core/package.json +3 -2
- package/corpus/core/src/automations/service.ts +41 -4
- package/corpus/core/src/cli/mcp-config-writers.ts +27 -21
- package/corpus/core/src/client/agent-page/AgentJobsTab.tsx +187 -85
- package/corpus/core/src/client/agent-page/AutomationDetailsDialog.tsx +208 -0
- package/corpus/core/src/client/agent-page/AutomationScheduleDialog.tsx +180 -0
- package/corpus/core/src/client/agent-page/TimezoneSelect.tsx +241 -0
- package/corpus/core/src/client/agent-page/use-jobs.ts +50 -10
- package/corpus/core/src/client/org/OrgSwitcher.tsx +26 -0
- package/corpus/core/src/client/settings/AccountSettingsCard.tsx +35 -31
- package/corpus/core/src/client/settings/SchedulingTimezoneField.tsx +71 -0
- package/corpus/core/src/client/settings/SettingsRow.tsx +113 -0
- package/corpus/core/src/client/settings/agent-settings-search.ts +2 -1
- package/corpus/core/src/client/settings/index.ts +6 -0
- package/corpus/core/src/integrations/identity.ts +2 -12
- package/corpus/core/src/jobs/actions/list-automation-runs.ts +33 -0
- package/corpus/core/src/jobs/actions/list-recurring-jobs.ts +27 -6
- package/corpus/core/src/jobs/actions/manage-recurring-job.ts +41 -9
- package/corpus/core/src/jobs/background-automation-runner.ts +88 -0
- package/corpus/core/src/jobs/cron.ts +52 -6
- package/corpus/core/src/jobs/frontmatter.ts +20 -0
- package/corpus/core/src/jobs/run-history.ts +242 -0
- package/corpus/core/src/jobs/scheduler.ts +73 -18
- package/corpus/core/src/jobs/tools.ts +54 -6
- package/corpus/core/src/localization/actions/get-localization-preference.ts +2 -2
- package/corpus/core/src/localization/actions/set-localization-preference.ts +49 -11
- package/corpus/core/src/localization/default-messages.ts +27 -0
- package/corpus/core/src/localization/shared.ts +39 -6
- package/corpus/core/src/localization/user-timezone.ts +36 -0
- package/corpus/core/src/org/app-roles-handlers.ts +1 -9
- package/corpus/core/src/org/index.ts +2 -0
- package/corpus/core/src/org/membership.ts +18 -0
- package/corpus/core/src/review/actions/create-review-comment.ts +4 -1
- package/corpus/core/src/review/actions/reply-review-comment.ts +4 -1
- package/corpus/core/src/review/index.ts +5 -0
- package/corpus/core/src/review/notifications.ts +163 -0
- package/corpus/core/src/review/types.ts +7 -0
- package/corpus/core/src/server/action-discovery.ts +4 -0
- package/corpus/core/src/server/activity-notifications.ts +174 -0
- package/corpus/core/src/server/app-name.ts +31 -0
- package/corpus/core/src/server/better-auth-instance.ts +4 -4
- package/corpus/core/src/server/email-templates.ts +70 -14
- package/corpus/core/src/server/email.ts +66 -3
- package/corpus/core/src/server/index.ts +10 -0
- package/corpus/core/src/sharing/index.ts +5 -0
- package/corpus/core/src/sharing/recipients.ts +67 -0
- package/corpus/core/src/triggers/actions/list-automations.ts +27 -8
- package/corpus/core/src/triggers/actions/manage-automation.ts +17 -6
- package/corpus/core/src/triggers/actions.ts +16 -4
- package/corpus/core/src/triggers/dispatcher.ts +44 -30
- package/corpus/core/src/triggers/routes.ts +19 -6
- package/corpus/core/src/vitest-config.ts +68 -0
- package/corpus/core/vitest.config.ts +5 -0
- package/corpus/templates/analytics/app/pages/Settings.tsx +37 -53
- package/corpus/templates/analytics/changelog/2026-08-01-new-error-alerts-now-reach-you-by-email-and-a-scheduled-dash.md +6 -0
- package/corpus/templates/analytics/server/jobs/dashboard-report.ts +43 -0
- package/corpus/templates/analytics/server/lib/error-capture.ts +5 -1
- package/corpus/templates/analytics/vitest.config.ts +16 -12
- package/corpus/templates/assets/actions/refresh-generation-run.ts +3 -0
- package/corpus/templates/assets/app/components/ui/switch.tsx +1 -0
- package/corpus/templates/assets/app/hooks/use-assets-prefs.ts +55 -0
- package/corpus/templates/assets/app/i18n-data.ts +30 -0
- package/corpus/templates/assets/app/routes/settings.tsx +58 -55
- package/corpus/templates/assets/changelog/2026-08-01-get-an-email-when-a-generation-you-started-finishes-or-fails.md +6 -0
- package/corpus/templates/assets/server/lib/generation-run-notifications.ts +89 -0
- package/corpus/templates/assets/server/lib/video-runs.ts +8 -1
- package/corpus/templates/assets/server/routes/_agent-native/assets/user-prefs.get.ts +17 -0
- package/corpus/templates/assets/server/routes/_agent-native/assets/user-prefs.put.ts +36 -0
- package/corpus/templates/assets/shared/assets-user-prefs.ts +10 -0
- package/corpus/templates/assets/vitest.config.ts +16 -12
- package/corpus/templates/brain/app/routes/settings.tsx +15 -15
- package/corpus/templates/brain/vitest.config.ts +16 -12
- package/corpus/templates/calendar/app/pages/Settings.tsx +32 -40
- package/corpus/templates/calendar/changelog/2026-08-01-language-and-appearance-now-sit-together-in-one-preferences-.md +6 -0
- package/corpus/templates/calendar/vitest.config.ts +16 -12
- package/corpus/templates/chat/app/routes/settings.tsx +14 -22
- package/corpus/templates/chat/vitest.config.ts +9 -0
- package/corpus/templates/clips/.agents/skills/video-editing/SKILL.md +31 -0
- package/corpus/templates/clips/AGENTS.md +1 -0
- package/corpus/templates/clips/actions/add-comment.ts +12 -1
- package/corpus/templates/clips/actions/create-meeting.ts +2 -2
- package/corpus/templates/clips/actions/create-recording.ts +2 -2
- package/corpus/templates/clips/actions/finalize-recording.ts +42 -6
- package/corpus/templates/clips/actions/generate-filmstrip.ts +154 -0
- package/corpus/templates/clips/actions/get-recording-player-data.ts +6 -0
- package/corpus/templates/clips/actions/import-loom-recording.ts +2 -2
- package/corpus/templates/clips/actions/lib/ensure-recording-filmstrip.ts +258 -0
- package/corpus/templates/clips/actions/lib/ensure-seekable-video.ts +11 -3
- package/corpus/templates/clips/actions/react-to-comment.ts +13 -0
- package/corpus/templates/clips/actions/react-to-recording.ts +10 -1
- package/corpus/templates/clips/actions/reply-to-comment.ts +12 -1
- package/corpus/templates/clips/actions/stitch-recordings.ts +2 -2
- package/corpus/templates/clips/app/components/editor/editor-layout.tsx +152 -23
- package/corpus/templates/clips/app/components/editor/waveform.tsx +237 -75
- package/corpus/templates/clips/app/components/recorder/recorder-engine.ts +23 -1
- package/corpus/templates/clips/app/components/settings/ai-setup-section.tsx +441 -0
- package/corpus/templates/clips/app/components/settings/slack-section.tsx +329 -0
- package/corpus/templates/clips/app/components/settings/types.ts +14 -0
- package/corpus/templates/clips/app/components/settings/video-storage-section.tsx +443 -0
- package/corpus/templates/clips/app/hooks/use-secret-status.ts +69 -0
- package/corpus/templates/clips/app/i18n/en-US.ts +9 -1
- package/corpus/templates/clips/app/lib/video-filmstrip.ts +262 -0
- package/corpus/templates/clips/app/routes/_app.settings._index.tsx +165 -1351
- package/corpus/templates/clips/changelog/2026-07-27-saved-clips-now-resume-from-the-last-uploaded-byte-after-an-.md +6 -0
- package/corpus/templates/clips/changelog/2026-07-31-editor-timeline-now-shows-video-frames-behind-the-waveform-g.md +6 -0
- package/corpus/templates/clips/changelog/2026-07-31-email-notifications-now-go-out-when-someone-comments-on-or-r.md +6 -0
- package/corpus/templates/clips/changelog/2026-07-31-you-can-now-set-a-default-visibility-for-new-recordings-in-s.md +6 -0
- package/corpus/templates/clips/changelog/2026-08-01-email-notifications-for-comments-and-reactions-now-actually-.md +6 -0
- package/corpus/templates/clips/changelog/2026-08-03-the-desktop-app-now-opens-directly-beneath-its-tray-icon-on-.md +6 -0
- package/corpus/templates/clips/chrome-extension/package.json +1 -0
- package/corpus/templates/clips/chrome-extension/vitest.config.ts +9 -0
- package/corpus/templates/clips/desktop/package.json +1 -0
- package/corpus/templates/clips/desktop/src/app.tsx +17 -1
- package/corpus/templates/clips/desktop/src/lib/recorder.ts +331 -36
- package/corpus/templates/clips/desktop/src/lib/upload-recovery.ts +129 -0
- package/corpus/templates/clips/desktop/src-tauri/src/native_screen.rs +815 -15
- package/corpus/templates/clips/desktop/src-tauri/src/tray.rs +61 -11
- package/corpus/templates/clips/desktop/vitest.config.ts +9 -0
- package/corpus/templates/clips/server/db/schema.ts +17 -0
- package/corpus/templates/clips/server/jobs/media-verification.ts +4 -0
- package/corpus/templates/clips/server/lib/activity-notifications.ts +185 -0
- package/corpus/templates/clips/server/lib/recording-media-cleanup.ts +2 -0
- package/corpus/templates/clips/server/lib/recording-upload-state.ts +46 -9
- package/corpus/templates/clips/server/lib/recordings.ts +24 -0
- package/corpus/templates/clips/server/lib/resumable-session.ts +10 -4
- package/corpus/templates/clips/server/lib/transactional-email-templates.ts +109 -0
- package/corpus/templates/clips/server/lib/upload-interruption.ts +8 -0
- package/corpus/templates/clips/server/lib/upload-lease.ts +50 -4
- package/corpus/templates/clips/server/lib/video-filmstrip-sprite.ts +207 -0
- package/corpus/templates/clips/server/lib/video-remux.ts +13 -4
- package/corpus/templates/clips/server/plugins/db.ts +16 -0
- package/corpus/templates/clips/server/routes/api/_agent-native-background/post-finalize-worker.post.ts +4 -0
- package/corpus/templates/clips/server/routes/api/uploads/[recordingId]/abort.post.ts +123 -36
- package/corpus/templates/clips/server/routes/api/uploads/[recordingId]/chunk.post.ts +159 -29
- package/corpus/templates/clips/server/routes/api/uploads/[recordingId]/interrupt.post.ts +197 -0
- package/corpus/templates/clips/server/routes/api/uploads/[recordingId]/reset-chunks.post.ts +193 -39
- package/corpus/templates/clips/server/routes/api/uploads/[recordingId]/resume.get.ts +191 -12
- package/corpus/templates/clips/shared/clips-ai-prefs.ts +6 -0
- package/corpus/templates/clips/shared/feature-flags.ts +12 -0
- package/corpus/templates/clips/shared/recording-core.ts +4 -0
- package/corpus/templates/clips/vitest.config.ts +23 -19
- package/corpus/templates/content/actions/add-comment.ts +15 -1
- package/corpus/templates/content/actions/list-documents.ts +1 -0
- package/corpus/templates/content/app/components/editor/DocumentEditor.tsx +5 -2
- package/corpus/templates/content/app/components/editor/DocumentToolbar.tsx +50 -1
- package/corpus/templates/content/app/components/ui/switch.tsx +1 -0
- package/corpus/templates/content/app/hooks/use-content-prefs.ts +55 -0
- package/corpus/templates/content/app/i18n-data.ts +40 -0
- package/corpus/templates/content/app/routes/_app.settings.tsx +47 -18
- package/corpus/templates/content/changelog/2026-07-30-breadcrumb-menus-now-hide-internal-system-pages.md +6 -0
- package/corpus/templates/content/changelog/2026-08-01-get-an-email-when-someone-comments-on-replies-in-or-mentions.md +6 -0
- package/corpus/templates/content/server/lib/comment-notifications.ts +146 -0
- package/corpus/templates/content/server/routes/_agent-native/content/user-prefs.get.ts +17 -0
- package/corpus/templates/content/server/routes/_agent-native/content/user-prefs.put.ts +36 -0
- package/corpus/templates/content/shared/content-user-prefs.ts +11 -0
- package/corpus/templates/content/vitest.config.ts +20 -16
- package/corpus/templates/crm/app/routes/settings.tsx +14 -22
- package/corpus/templates/crm/vitest.config.ts +8 -4
- package/corpus/templates/design/app/routes/settings.tsx +14 -22
- package/corpus/templates/design/changelog/2026-08-01-review-comments-replies-and-mentions-now-send-an-email-to-th.md +6 -0
- package/corpus/templates/design/vitest.config.ts +18 -12
- package/corpus/templates/dispatch/app/routes/settings.tsx +26 -40
- package/corpus/templates/dispatch/vitest.config.ts +11 -7
- package/corpus/templates/factory/app/routes/settings.tsx +14 -22
- package/corpus/templates/factory/vitest.config.ts +9 -0
- package/corpus/templates/forms/app/routes/_app.settings.tsx +14 -22
- package/corpus/templates/forms/vitest.config.ts +27 -23
- package/corpus/templates/macros/app/routes/settings.tsx +14 -22
- package/corpus/templates/macros/vitest.config.ts +16 -12
- package/corpus/templates/mail/app/pages/SettingsPage.tsx +14 -16
- package/corpus/templates/mail/vitest.config.ts +16 -12
- package/corpus/templates/plan/app/routes/settings.tsx +30 -44
- package/corpus/templates/plan/vitest.config.ts +24 -20
- package/corpus/templates/slides/actions/add-slide-comment.ts +12 -1
- package/corpus/templates/slides/app/components/ui/switch.tsx +1 -0
- package/corpus/templates/slides/app/hooks/use-slides-prefs.ts +55 -0
- package/corpus/templates/slides/app/i18n/en-US.ts +4 -0
- package/corpus/templates/slides/app/routes/settings.tsx +45 -22
- package/corpus/templates/slides/changelog/2026-08-01-get-an-email-when-someone-comments-on-or-replies-in-your-dec.md +6 -0
- package/corpus/templates/slides/server/lib/comment-notifications.ts +182 -0
- package/corpus/templates/slides/server/routes/_agent-native/slides/user-prefs.get.ts +16 -0
- package/corpus/templates/slides/server/routes/_agent-native/slides/user-prefs.put.ts +36 -0
- package/corpus/templates/slides/shared/slides-user-prefs.ts +11 -0
- package/corpus/templates/slides/vitest.config.ts +17 -13
- package/corpus/templates/tasks/app/components/layout/Sidebar.tsx +2 -0
- package/corpus/templates/tasks/app/routes/settings.tsx +14 -22
- package/corpus/templates/tasks/vitest.config.ts +17 -13
- package/corpus/toolkit/CHANGELOG.md +9 -0
- package/corpus/toolkit/package.json +1 -1
- package/corpus/toolkit/vitest.config.ts +3 -0
- package/dist/automations/service.d.ts +2 -0
- package/dist/automations/service.d.ts.map +1 -1
- package/dist/automations/service.js +29 -4
- package/dist/automations/service.js.map +1 -1
- package/dist/cli/mcp-config-writers.d.ts.map +1 -1
- package/dist/cli/mcp-config-writers.js +24 -23
- package/dist/cli/mcp-config-writers.js.map +1 -1
- package/dist/client/agent-page/AgentJobsTab.d.ts.map +1 -1
- package/dist/client/agent-page/AgentJobsTab.js +86 -29
- package/dist/client/agent-page/AgentJobsTab.js.map +1 -1
- package/dist/client/agent-page/AutomationDetailsDialog.d.ts +21 -0
- package/dist/client/agent-page/AutomationDetailsDialog.d.ts.map +1 -0
- package/dist/client/agent-page/AutomationDetailsDialog.js +33 -0
- package/dist/client/agent-page/AutomationDetailsDialog.js.map +1 -0
- package/dist/client/agent-page/AutomationScheduleDialog.d.ts +15 -0
- package/dist/client/agent-page/AutomationScheduleDialog.d.ts.map +1 -0
- package/dist/client/agent-page/AutomationScheduleDialog.js +46 -0
- package/dist/client/agent-page/AutomationScheduleDialog.js.map +1 -0
- package/dist/client/agent-page/TimezoneSelect.d.ts +17 -0
- package/dist/client/agent-page/TimezoneSelect.d.ts.map +1 -0
- package/dist/client/agent-page/TimezoneSelect.js +107 -0
- package/dist/client/agent-page/TimezoneSelect.js.map +1 -0
- package/dist/client/agent-page/use-jobs.d.ts +20 -7
- package/dist/client/agent-page/use-jobs.d.ts.map +1 -1
- package/dist/client/agent-page/use-jobs.js +24 -4
- package/dist/client/agent-page/use-jobs.js.map +1 -1
- package/dist/client/org/OrgSwitcher.d.ts +8 -1
- package/dist/client/org/OrgSwitcher.d.ts.map +1 -1
- package/dist/client/org/OrgSwitcher.js +8 -2
- package/dist/client/org/OrgSwitcher.js.map +1 -1
- package/dist/client/settings/AccountSettingsCard.d.ts.map +1 -1
- package/dist/client/settings/AccountSettingsCard.js +10 -9
- package/dist/client/settings/AccountSettingsCard.js.map +1 -1
- package/dist/client/settings/SchedulingTimezoneField.d.ts +10 -0
- package/dist/client/settings/SchedulingTimezoneField.d.ts.map +1 -0
- package/dist/client/settings/SchedulingTimezoneField.js +30 -0
- package/dist/client/settings/SchedulingTimezoneField.js.map +1 -0
- package/dist/client/settings/SettingsRow.d.ts +36 -0
- package/dist/client/settings/SettingsRow.d.ts.map +1 -0
- package/dist/client/settings/SettingsRow.js +15 -0
- package/dist/client/settings/SettingsRow.js.map +1 -0
- package/dist/client/settings/agent-settings-search.d.ts.map +1 -1
- package/dist/client/settings/agent-settings-search.js +1 -1
- package/dist/client/settings/agent-settings-search.js.map +1 -1
- package/dist/client/settings/index.d.ts +1 -0
- package/dist/client/settings/index.d.ts.map +1 -1
- package/dist/client/settings/index.js +1 -0
- package/dist/client/settings/index.js.map +1 -1
- package/dist/collab/struct-routes.d.ts +1 -1
- package/dist/file-upload/actions/upload-image.d.ts +1 -1
- package/dist/integrations/identity.d.ts.map +1 -1
- package/dist/integrations/identity.js +2 -11
- package/dist/integrations/identity.js.map +1 -1
- package/dist/jobs/actions/list-automation-runs.d.ts +8 -0
- package/dist/jobs/actions/list-automation-runs.d.ts.map +1 -0
- package/dist/jobs/actions/list-automation-runs.js +29 -0
- package/dist/jobs/actions/list-automation-runs.js.map +1 -0
- package/dist/jobs/actions/list-recurring-jobs.d.ts +2 -0
- package/dist/jobs/actions/list-recurring-jobs.d.ts.map +1 -1
- package/dist/jobs/actions/list-recurring-jobs.js +20 -7
- package/dist/jobs/actions/list-recurring-jobs.js.map +1 -1
- package/dist/jobs/actions/manage-recurring-job.d.ts +2 -0
- package/dist/jobs/actions/manage-recurring-job.js +30 -9
- package/dist/jobs/actions/manage-recurring-job.js.map +1 -1
- package/dist/jobs/background-automation-runner.d.ts.map +1 -1
- package/dist/jobs/background-automation-runner.js +60 -0
- package/dist/jobs/background-automation-runner.js.map +1 -1
- package/dist/jobs/cron.d.ts +16 -2
- package/dist/jobs/cron.d.ts.map +1 -1
- package/dist/jobs/cron.js +43 -6
- package/dist/jobs/cron.js.map +1 -1
- package/dist/jobs/frontmatter.d.ts +12 -0
- package/dist/jobs/frontmatter.d.ts.map +1 -1
- package/dist/jobs/frontmatter.js +8 -0
- package/dist/jobs/frontmatter.js.map +1 -1
- package/dist/jobs/run-history.d.ts +56 -0
- package/dist/jobs/run-history.d.ts.map +1 -0
- package/dist/jobs/run-history.js +169 -0
- package/dist/jobs/run-history.js.map +1 -0
- package/dist/jobs/scheduler.d.ts.map +1 -1
- package/dist/jobs/scheduler.js +51 -18
- package/dist/jobs/scheduler.js.map +1 -1
- package/dist/jobs/tools.d.ts.map +1 -1
- package/dist/jobs/tools.js +37 -6
- package/dist/jobs/tools.js.map +1 -1
- package/dist/localization/actions/get-localization-preference.d.ts +1 -2
- package/dist/localization/actions/get-localization-preference.d.ts.map +1 -1
- package/dist/localization/actions/get-localization-preference.js.map +1 -1
- package/dist/localization/actions/set-localization-preference.d.ts +3 -3
- package/dist/localization/actions/set-localization-preference.d.ts.map +1 -1
- package/dist/localization/actions/set-localization-preference.js +33 -8
- package/dist/localization/actions/set-localization-preference.js.map +1 -1
- package/dist/localization/default-messages.d.ts +25 -0
- package/dist/localization/default-messages.d.ts.map +1 -1
- package/dist/localization/default-messages.js +25 -0
- package/dist/localization/default-messages.js.map +1 -1
- package/dist/localization/shared.d.ts +16 -1
- package/dist/localization/shared.d.ts.map +1 -1
- package/dist/localization/shared.js +22 -4
- package/dist/localization/shared.js.map +1 -1
- package/dist/localization/user-timezone.d.ts +11 -0
- package/dist/localization/user-timezone.d.ts.map +1 -0
- package/dist/localization/user-timezone.js +31 -0
- package/dist/localization/user-timezone.js.map +1 -0
- package/dist/notifications/routes.d.ts +1 -1
- package/dist/observability/routes.d.ts +1 -1
- package/dist/org/app-roles-handlers.d.ts.map +1 -1
- package/dist/org/app-roles-handlers.js +1 -8
- package/dist/org/app-roles-handlers.js.map +1 -1
- package/dist/org/index.d.ts +1 -0
- package/dist/org/index.d.ts.map +1 -1
- package/dist/org/index.js +1 -0
- package/dist/org/index.js.map +1 -1
- package/dist/org/membership.d.ts +6 -0
- package/dist/org/membership.d.ts.map +1 -0
- package/dist/org/membership.js +16 -0
- package/dist/org/membership.js.map +1 -0
- package/dist/provider-api/actions/custom-provider-registration.d.ts +10 -10
- package/dist/provider-api/actions/provider-api.d.ts +8 -8
- package/dist/provider-api/corpus-jobs.d.ts +2 -2
- package/dist/resources/handlers.d.ts +1 -1
- package/dist/review/actions/create-review-comment.d.ts +32 -1
- package/dist/review/actions/create-review-comment.d.ts.map +1 -1
- package/dist/review/actions/create-review-comment.js +3 -1
- package/dist/review/actions/create-review-comment.js.map +1 -1
- package/dist/review/actions/reply-review-comment.d.ts +32 -1
- package/dist/review/actions/reply-review-comment.d.ts.map +1 -1
- package/dist/review/actions/reply-review-comment.js +3 -1
- package/dist/review/actions/reply-review-comment.js.map +1 -1
- package/dist/review/index.d.ts +1 -0
- package/dist/review/index.d.ts.map +1 -1
- package/dist/review/index.js +1 -0
- package/dist/review/index.js.map +1 -1
- package/dist/review/notifications.d.ts +24 -0
- package/dist/review/notifications.d.ts.map +1 -0
- package/dist/review/notifications.js +123 -0
- package/dist/review/notifications.js.map +1 -0
- package/dist/review/types.d.ts +5 -0
- package/dist/review/types.d.ts.map +1 -1
- package/dist/review/types.js.map +1 -1
- package/dist/server/action-discovery.d.ts.map +1 -1
- package/dist/server/action-discovery.js +4 -0
- package/dist/server/action-discovery.js.map +1 -1
- package/dist/server/activity-notifications.d.ts +61 -0
- package/dist/server/activity-notifications.d.ts.map +1 -0
- package/dist/server/activity-notifications.js +103 -0
- package/dist/server/activity-notifications.js.map +1 -0
- package/dist/server/agent-engine-api-key-route.d.ts +1 -1
- package/dist/server/app-name.d.ts +18 -0
- package/dist/server/app-name.d.ts.map +1 -1
- package/dist/server/app-name.js +31 -0
- package/dist/server/app-name.js.map +1 -1
- package/dist/server/better-auth-instance.js +4 -4
- package/dist/server/better-auth-instance.js.map +1 -1
- package/dist/server/email-templates.d.ts +12 -0
- package/dist/server/email-templates.d.ts.map +1 -1
- package/dist/server/email-templates.js +57 -14
- package/dist/server/email-templates.js.map +1 -1
- package/dist/server/email.d.ts +12 -0
- package/dist/server/email.d.ts.map +1 -1
- package/dist/server/email.js +52 -5
- package/dist/server/email.js.map +1 -1
- package/dist/server/index.d.ts +1 -0
- package/dist/server/index.d.ts.map +1 -1
- package/dist/server/index.js +1 -0
- package/dist/server/index.js.map +1 -1
- package/dist/sharing/index.d.ts +1 -0
- package/dist/sharing/index.d.ts.map +1 -1
- package/dist/sharing/index.js +1 -0
- package/dist/sharing/index.js.map +1 -1
- package/dist/sharing/recipients.d.ts +31 -0
- package/dist/sharing/recipients.d.ts.map +1 -0
- package/dist/sharing/recipients.js +40 -0
- package/dist/sharing/recipients.js.map +1 -0
- package/dist/templates/chat/app/routes/settings.tsx +14 -22
- package/dist/templates/chat/vitest.config.ts +9 -0
- package/dist/triggers/actions/list-automations.d.ts +2 -0
- package/dist/triggers/actions/list-automations.d.ts.map +1 -1
- package/dist/triggers/actions/list-automations.js +19 -8
- package/dist/triggers/actions/list-automations.js.map +1 -1
- package/dist/triggers/actions/manage-automation.d.ts +6 -0
- package/dist/triggers/actions/manage-automation.js +12 -6
- package/dist/triggers/actions/manage-automation.js.map +1 -1
- package/dist/triggers/actions.d.ts.map +1 -1
- package/dist/triggers/actions.js +15 -4
- package/dist/triggers/actions.js.map +1 -1
- package/dist/triggers/dispatcher.d.ts.map +1 -1
- package/dist/triggers/dispatcher.js +19 -17
- package/dist/triggers/dispatcher.js.map +1 -1
- package/dist/triggers/routes.d.ts.map +1 -1
- package/dist/triggers/routes.js +6 -6
- package/dist/triggers/routes.js.map +1 -1
- package/dist/vitest-config.d.ts +10 -0
- package/dist/vitest-config.d.ts.map +1 -0
- package/dist/vitest-config.js +55 -0
- package/dist/vitest-config.js.map +1 -0
- package/package.json +5 -4
- package/src/automations/service.ts +41 -4
- package/src/cli/mcp-config-writers.ts +27 -21
- package/src/client/agent-page/AgentJobsTab.tsx +187 -85
- package/src/client/agent-page/AutomationDetailsDialog.tsx +208 -0
- package/src/client/agent-page/AutomationScheduleDialog.tsx +180 -0
- package/src/client/agent-page/TimezoneSelect.tsx +241 -0
- package/src/client/agent-page/use-jobs.ts +50 -10
- package/src/client/org/OrgSwitcher.tsx +26 -0
- package/src/client/settings/AccountSettingsCard.tsx +35 -31
- package/src/client/settings/SchedulingTimezoneField.tsx +71 -0
- package/src/client/settings/SettingsRow.tsx +113 -0
- package/src/client/settings/agent-settings-search.ts +2 -1
- package/src/client/settings/index.ts +6 -0
- package/src/integrations/identity.ts +2 -12
- package/src/jobs/actions/list-automation-runs.ts +33 -0
- package/src/jobs/actions/list-recurring-jobs.ts +27 -6
- package/src/jobs/actions/manage-recurring-job.ts +41 -9
- package/src/jobs/background-automation-runner.ts +88 -0
- package/src/jobs/cron.ts +52 -6
- package/src/jobs/frontmatter.ts +20 -0
- package/src/jobs/run-history.ts +242 -0
- package/src/jobs/scheduler.ts +73 -18
- package/src/jobs/tools.ts +54 -6
- package/src/localization/actions/get-localization-preference.ts +2 -2
- package/src/localization/actions/set-localization-preference.ts +49 -11
- package/src/localization/default-messages.ts +27 -0
- package/src/localization/shared.ts +39 -6
- package/src/localization/user-timezone.ts +36 -0
- package/src/org/app-roles-handlers.ts +1 -9
- package/src/org/index.ts +2 -0
- package/src/org/membership.ts +18 -0
- package/src/review/actions/create-review-comment.ts +4 -1
- package/src/review/actions/reply-review-comment.ts +4 -1
- package/src/review/index.ts +5 -0
- package/src/review/notifications.ts +163 -0
- package/src/review/types.ts +7 -0
- package/src/server/action-discovery.ts +4 -0
- package/src/server/activity-notifications.ts +174 -0
- package/src/server/app-name.ts +31 -0
- package/src/server/better-auth-instance.ts +4 -4
- package/src/server/email-templates.ts +70 -14
- package/src/server/email.ts +66 -3
- package/src/server/index.ts +10 -0
- package/src/sharing/index.ts +5 -0
- package/src/sharing/recipients.ts +67 -0
- package/src/templates/chat/app/routes/settings.tsx +14 -22
- package/src/templates/chat/vitest.config.ts +9 -0
- package/src/triggers/actions/list-automations.ts +27 -8
- package/src/triggers/actions/manage-automation.ts +17 -6
- package/src/triggers/actions.ts +16 -4
- package/src/triggers/dispatcher.ts +44 -30
- package/src/triggers/routes.ts +19 -6
- package/src/vitest-config.ts +68 -0
|
@@ -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
|
-
):
|
|
203
|
+
): ResolvedLocalizationPreference {
|
|
174
204
|
if (value && typeof value === "object" && !Array.isArray(value)) {
|
|
175
|
-
const
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
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.
|
|
@@ -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
|
-
|
|
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
|
-
|
|
66
|
+
const reply = await insertReviewReply(
|
|
66
67
|
{
|
|
67
68
|
resourceType: args.resourceType,
|
|
68
69
|
resourceId: args.resourceId,
|
|
@@ -88,6 +89,8 @@ export default defineAction({
|
|
|
88
89
|
resourceId: args.resourceId,
|
|
89
90
|
},
|
|
90
91
|
);
|
|
92
|
+
|
|
93
|
+
return { ...reply, notified: await notifyReviewComment(reply) };
|
|
91
94
|
},
|
|
92
95
|
audit: {
|
|
93
96
|
target: (args, result) => {
|
|
@@ -20,6 +20,11 @@ export {
|
|
|
20
20
|
shouldRedactReviewIdentity,
|
|
21
21
|
} from "./identity.js";
|
|
22
22
|
export { extractReviewMentions, normalizeReviewMentions } from "./mentions.js";
|
|
23
|
+
export {
|
|
24
|
+
notifyReviewComment,
|
|
25
|
+
REVIEW_NOTIFICATION_PREFS_KEY,
|
|
26
|
+
type ReviewNotificationResult,
|
|
27
|
+
} from "./notifications.js";
|
|
23
28
|
export {
|
|
24
29
|
__resetReviewableResourcesForTests,
|
|
25
30
|
assertReviewableResourceAccess,
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Email notifications for review threads.
|
|
3
|
+
*
|
|
4
|
+
* Implemented here rather than in one app so every surface built on review
|
|
5
|
+
* comments (Design's review threads today) gets the same behavior. Recipient
|
|
6
|
+
* resolution, preference filtering, and delivery reporting come from the
|
|
7
|
+
* shared activity-notification helpers.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import {
|
|
11
|
+
notifyActivity,
|
|
12
|
+
runActivityNotification,
|
|
13
|
+
type ActivityNotificationResult,
|
|
14
|
+
} from "../server/activity-notifications.js";
|
|
15
|
+
import { getAppProductionUrl } from "../server/app-url.js";
|
|
16
|
+
import { emailStrong, renderEmail } from "../server/email-template.js";
|
|
17
|
+
import { sendEmail } from "../server/email.js";
|
|
18
|
+
import { filterRecipientsByResourceAccess } from "../sharing/recipients.js";
|
|
19
|
+
import {
|
|
20
|
+
getReviewableResource,
|
|
21
|
+
resolveReviewableResourceAccess,
|
|
22
|
+
} from "./registry.js";
|
|
23
|
+
import { queryReviewComments } from "./store.js";
|
|
24
|
+
import type { ReviewComment } from "./types.js";
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Shared across every review surface. Apps that want a user-facing toggle
|
|
28
|
+
* write `{ emailNotifications: boolean }` under this key; an absent value
|
|
29
|
+
* means opted in.
|
|
30
|
+
*/
|
|
31
|
+
export const REVIEW_NOTIFICATION_PREFS_KEY = "activity-notification-prefs";
|
|
32
|
+
|
|
33
|
+
const LOG_LABEL = "[review] comment notification";
|
|
34
|
+
const EXCERPT_LIMIT = 240;
|
|
35
|
+
|
|
36
|
+
function excerpt(body: string): string {
|
|
37
|
+
const collapsed = body.replace(/\s+/g, " ").trim();
|
|
38
|
+
return collapsed.length > EXCERPT_LIMIT
|
|
39
|
+
? `${collapsed.slice(0, EXCERPT_LIMIT - 1)}…`
|
|
40
|
+
: collapsed;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
async function resourceUrl(comment: ReviewComment): Promise<string> {
|
|
44
|
+
const registration = getReviewableResource(comment.resourceType);
|
|
45
|
+
const resolved = await registration?.resolveUrl?.(comment.resourceId);
|
|
46
|
+
return resolved?.trim() || getAppProductionUrl();
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function resourceLabel(comment: ReviewComment): string {
|
|
50
|
+
return (
|
|
51
|
+
getReviewableResource(comment.resourceType)?.displayName?.trim() ||
|
|
52
|
+
comment.resourceType
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
async function threadParticipants(comment: ReviewComment): Promise<string[]> {
|
|
57
|
+
// Scope is already established by the action that inserted the comment;
|
|
58
|
+
// participants are read unscoped so a viewer's narrower scope cannot hide a
|
|
59
|
+
// person who is genuinely in the thread.
|
|
60
|
+
const comments = await queryReviewComments({
|
|
61
|
+
resourceType: comment.resourceType,
|
|
62
|
+
resourceId: comment.resourceId,
|
|
63
|
+
scope: { userEmail: null, orgId: null },
|
|
64
|
+
bypassScope: true,
|
|
65
|
+
includeResolved: true,
|
|
66
|
+
});
|
|
67
|
+
return comments
|
|
68
|
+
.filter((entry) => entry.threadId === comment.threadId)
|
|
69
|
+
.map((entry) => entry.authorEmail)
|
|
70
|
+
.filter((email): email is string => Boolean(email));
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export type ReviewNotificationResult = ActivityNotificationResult;
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Email the resource owner, mentioned people, and — on a reply — everyone else
|
|
77
|
+
* already in the thread. Never throws: the comment is already persisted, and a
|
|
78
|
+
* rejection here would make the client retry and duplicate it.
|
|
79
|
+
*/
|
|
80
|
+
export async function notifyReviewComment(
|
|
81
|
+
comment: ReviewComment,
|
|
82
|
+
): Promise<ReviewNotificationResult> {
|
|
83
|
+
return runActivityNotification(LOG_LABEL, () =>
|
|
84
|
+
deliverReviewCommentEmails(comment),
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
async function deliverReviewCommentEmails(
|
|
89
|
+
comment: ReviewComment,
|
|
90
|
+
): Promise<ActivityNotificationResult> {
|
|
91
|
+
const mentioned = new Set(
|
|
92
|
+
comment.mentions
|
|
93
|
+
.map((mention) => mention.email?.trim().toLowerCase())
|
|
94
|
+
.filter((email): email is string => Boolean(email)),
|
|
95
|
+
);
|
|
96
|
+
|
|
97
|
+
const candidates = [comment.ownerEmail, ...mentioned];
|
|
98
|
+
const isReply = Boolean(comment.parentCommentId);
|
|
99
|
+
if (isReply) {
|
|
100
|
+
candidates.push(...(await threadParticipants(comment)));
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Mentions are caller-supplied and thread rows are historical; neither is an
|
|
104
|
+
// access grant. Re-check every address against the resource's current ACL
|
|
105
|
+
// before it can receive the comment body.
|
|
106
|
+
const allowed = await filterRecipientsByResourceAccess({
|
|
107
|
+
resourceType: comment.resourceType,
|
|
108
|
+
resourceId: comment.resourceId,
|
|
109
|
+
emails: candidates.filter((email): email is string => Boolean(email)),
|
|
110
|
+
orgId: comment.orgId,
|
|
111
|
+
// The review registry owns access for its types; unregistered ones resolve
|
|
112
|
+
// to null there, so nobody is notified rather than everybody.
|
|
113
|
+
resolveRole: (ctx) =>
|
|
114
|
+
resolveReviewableResourceAccess(
|
|
115
|
+
comment.resourceType,
|
|
116
|
+
comment.resourceId,
|
|
117
|
+
ctx,
|
|
118
|
+
),
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
const actor = comment.authorName?.trim() || comment.authorEmail || "Someone";
|
|
122
|
+
const label = resourceLabel(comment);
|
|
123
|
+
const url = await resourceUrl(comment);
|
|
124
|
+
|
|
125
|
+
return notifyActivity({
|
|
126
|
+
candidates: allowed,
|
|
127
|
+
actorEmail: comment.authorEmail,
|
|
128
|
+
preferenceKey: REVIEW_NOTIFICATION_PREFS_KEY,
|
|
129
|
+
logLabel: LOG_LABEL,
|
|
130
|
+
send: async (to) => {
|
|
131
|
+
const wasMentioned = mentioned.has(to);
|
|
132
|
+
const lead = wasMentioned
|
|
133
|
+
? `${emailStrong(actor)} mentioned you in a review comment on this ${label}.`
|
|
134
|
+
: isReply
|
|
135
|
+
? `${emailStrong(actor)} replied in a review thread on this ${label}.`
|
|
136
|
+
: `${emailStrong(actor)} left a review comment on this ${label}.`;
|
|
137
|
+
|
|
138
|
+
const { html, text } = renderEmail({
|
|
139
|
+
preheader: `${actor} commented on this ${label}.`,
|
|
140
|
+
heading: wasMentioned
|
|
141
|
+
? "You were mentioned in a review"
|
|
142
|
+
: isReply
|
|
143
|
+
? "New reply in a review thread"
|
|
144
|
+
: "New review comment",
|
|
145
|
+
paragraphs: [lead, `"${excerpt(comment.body)}"`],
|
|
146
|
+
cta: { label: "Open review", url },
|
|
147
|
+
footer:
|
|
148
|
+
"You received this because you own, were mentioned in, or participated in this review thread.",
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
await sendEmail({
|
|
152
|
+
to,
|
|
153
|
+
subject: wasMentioned
|
|
154
|
+
? `${actor} mentioned you in a review comment`
|
|
155
|
+
: isReply
|
|
156
|
+
? `${actor} replied to a review thread`
|
|
157
|
+
: `${actor} left a review comment`,
|
|
158
|
+
html,
|
|
159
|
+
text,
|
|
160
|
+
});
|
|
161
|
+
},
|
|
162
|
+
});
|
|
163
|
+
}
|
|
@@ -36,6 +36,13 @@ export interface ReviewResourceContext {
|
|
|
36
36
|
export interface ReviewableResourceRegistration {
|
|
37
37
|
type: string;
|
|
38
38
|
displayName?: string;
|
|
39
|
+
/**
|
|
40
|
+
* Deep link to the resource, used by review notification emails. Without it
|
|
41
|
+
* an email can only link to the app root.
|
|
42
|
+
*/
|
|
43
|
+
resolveUrl?: (
|
|
44
|
+
resourceId: string,
|
|
45
|
+
) => Promise<string | null | undefined> | string | null | undefined;
|
|
39
46
|
resolveAccess?: (
|
|
40
47
|
resourceId: string,
|
|
41
48
|
ctx?: ReviewResourceContext,
|
|
@@ -625,6 +625,10 @@ export async function mergeCoreSharingActions(
|
|
|
625
625
|
"manage-recurring-job",
|
|
626
626
|
() => import("../jobs/actions/manage-recurring-job.js"),
|
|
627
627
|
],
|
|
628
|
+
[
|
|
629
|
+
"list-automation-runs",
|
|
630
|
+
() => import("../jobs/actions/list-automation-runs.js"),
|
|
631
|
+
],
|
|
628
632
|
[
|
|
629
633
|
"list-automations",
|
|
630
634
|
() => import("../triggers/actions/list-automations.js"),
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared plumbing for collaboration emails (comments, replies, reactions,
|
|
3
|
+
* mentions).
|
|
4
|
+
*
|
|
5
|
+
* Two things every app was about to reimplement live here: resolving who
|
|
6
|
+
* should receive an activity email, and reporting delivery honestly. A
|
|
7
|
+
* workspace with no email provider is a *different* outcome from "nobody
|
|
8
|
+
* wanted this email" — collapsing both into an empty success is what let a
|
|
9
|
+
* dead notification toggle ship unnoticed.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { getUserSetting } from "../settings/user-settings.js";
|
|
13
|
+
import { isEmailConfigured } from "./email.js";
|
|
14
|
+
|
|
15
|
+
export type ActivityDeliveryFailure = { email: string; error: string };
|
|
16
|
+
|
|
17
|
+
export type ActivityNotificationStatus =
|
|
18
|
+
| "delivered"
|
|
19
|
+
| "delivery-failed"
|
|
20
|
+
| "email-not-configured"
|
|
21
|
+
| "no-recipients"
|
|
22
|
+
| "notification-error";
|
|
23
|
+
|
|
24
|
+
export type ActivityNotificationResult = {
|
|
25
|
+
status: ActivityNotificationStatus;
|
|
26
|
+
sent: string[];
|
|
27
|
+
failed: ActivityDeliveryFailure[];
|
|
28
|
+
/** Set only on `notification-error`: why recipients could not be resolved. */
|
|
29
|
+
error?: string;
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
/** Default field read from an app's per-user preference blob. */
|
|
33
|
+
const DEFAULT_PREFERENCE_FIELD = "emailNotifications";
|
|
34
|
+
|
|
35
|
+
export interface ResolveActivityRecipientsInput {
|
|
36
|
+
/** Owner, thread participants, mentions — duplicates and blanks are fine. */
|
|
37
|
+
candidates: (string | null | undefined)[];
|
|
38
|
+
/** The person who caused the activity. Never emailed about their own action. */
|
|
39
|
+
actorEmail?: string | null;
|
|
40
|
+
/** App-owned user settings key, e.g. `clips-user-prefs`. */
|
|
41
|
+
preferenceKey: string;
|
|
42
|
+
/** Field inside that blob. Absent or non-`false` means opted in. */
|
|
43
|
+
preferenceField?: string;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function normalizeEmail(value: string | null | undefined): string {
|
|
47
|
+
return (value ?? "").trim().toLowerCase();
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
async function wantsActivityEmail(
|
|
51
|
+
email: string,
|
|
52
|
+
preferenceKey: string,
|
|
53
|
+
preferenceField: string,
|
|
54
|
+
): Promise<boolean> {
|
|
55
|
+
const prefs = await getUserSetting(email, preferenceKey);
|
|
56
|
+
// A user who never opened settings has no stored blob; treat that as opted
|
|
57
|
+
// in rather than silently dropping their notifications.
|
|
58
|
+
return prefs?.[preferenceField] !== false;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Normalize, dedupe, drop the actor, and filter by each recipient's stored
|
|
63
|
+
* preference. Order follows first appearance in `candidates`.
|
|
64
|
+
*/
|
|
65
|
+
export async function resolveActivityRecipients({
|
|
66
|
+
candidates,
|
|
67
|
+
actorEmail,
|
|
68
|
+
preferenceKey,
|
|
69
|
+
preferenceField = DEFAULT_PREFERENCE_FIELD,
|
|
70
|
+
}: ResolveActivityRecipientsInput): Promise<string[]> {
|
|
71
|
+
const actor = normalizeEmail(actorEmail);
|
|
72
|
+
const unique = new Set<string>();
|
|
73
|
+
for (const candidate of candidates) {
|
|
74
|
+
const email = normalizeEmail(candidate);
|
|
75
|
+
if (!email || email === actor || !email.includes("@")) continue;
|
|
76
|
+
unique.add(email);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const decisions = await Promise.all(
|
|
80
|
+
[...unique].map(async (email) =>
|
|
81
|
+
(await wantsActivityEmail(email, preferenceKey, preferenceField))
|
|
82
|
+
? email
|
|
83
|
+
: null,
|
|
84
|
+
),
|
|
85
|
+
);
|
|
86
|
+
return decisions.filter((email): email is string => email !== null);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
export interface NotifyActivityInput extends ResolveActivityRecipientsInput {
|
|
90
|
+
/** Sends one email. Throwing marks that recipient failed, not the batch. */
|
|
91
|
+
send: (to: string) => Promise<unknown>;
|
|
92
|
+
/** Prefix for the delivery-failure log line, e.g. `[slides]`. */
|
|
93
|
+
logLabel?: string;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Resolve recipients and deliver one email each. Returns a status that
|
|
98
|
+
* distinguishes "no email provider" from "nobody to email" from a real send.
|
|
99
|
+
*/
|
|
100
|
+
export async function notifyActivity({
|
|
101
|
+
send,
|
|
102
|
+
logLabel,
|
|
103
|
+
...resolve
|
|
104
|
+
}: NotifyActivityInput): Promise<ActivityNotificationResult> {
|
|
105
|
+
if (!(await isEmailConfigured())) {
|
|
106
|
+
return { status: "email-not-configured", sent: [], failed: [] };
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const recipients = await resolveActivityRecipients(resolve);
|
|
110
|
+
if (recipients.length === 0) {
|
|
111
|
+
return { status: "no-recipients", sent: [], failed: [] };
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
const sent: string[] = [];
|
|
115
|
+
const failed: ActivityDeliveryFailure[] = [];
|
|
116
|
+
for (const to of recipients) {
|
|
117
|
+
try {
|
|
118
|
+
await send(to);
|
|
119
|
+
sent.push(to);
|
|
120
|
+
} catch (error) {
|
|
121
|
+
failed.push({
|
|
122
|
+
email: to,
|
|
123
|
+
error: error instanceof Error ? error.message : String(error),
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
if (failed.length > 0) {
|
|
129
|
+
console.error(
|
|
130
|
+
`${logLabel ?? "[activity-notifications]"} delivery failed for ${failed
|
|
131
|
+
.map((failure) => `${failure.email} (${failure.error})`)
|
|
132
|
+
.join(", ")}`,
|
|
133
|
+
);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// Every recipient failing is an outage, not a delivery. Callers that log or
|
|
137
|
+
// surface this must not see it as the same outcome as a successful send.
|
|
138
|
+
return {
|
|
139
|
+
status: sent.length === 0 ? "delivery-failed" : "delivered",
|
|
140
|
+
sent,
|
|
141
|
+
failed,
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Run an activity notification without letting it fail the write that caused
|
|
147
|
+
* it. The comment/reaction is already persisted by the time notification runs;
|
|
148
|
+
* rejecting here makes the client roll back and retry, which duplicates the
|
|
149
|
+
* row. The error still surfaces — as a distinct `notification-error` status,
|
|
150
|
+
* never as a silent success.
|
|
151
|
+
*/
|
|
152
|
+
export async function runActivityNotification<
|
|
153
|
+
T extends {
|
|
154
|
+
status: string;
|
|
155
|
+
sent: string[];
|
|
156
|
+
failed: ActivityDeliveryFailure[];
|
|
157
|
+
},
|
|
158
|
+
>(
|
|
159
|
+
logLabel: string,
|
|
160
|
+
resolve: () => Promise<T>,
|
|
161
|
+
): Promise<T | ActivityNotificationResult> {
|
|
162
|
+
try {
|
|
163
|
+
return await resolve();
|
|
164
|
+
} catch (error) {
|
|
165
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
166
|
+
console.error(`${logLabel} could not be resolved: ${message}`);
|
|
167
|
+
return {
|
|
168
|
+
status: "notification-error",
|
|
169
|
+
error: message,
|
|
170
|
+
sent: [],
|
|
171
|
+
failed: [],
|
|
172
|
+
};
|
|
173
|
+
}
|
|
174
|
+
}
|
|
@@ -51,3 +51,34 @@ export function getAppName(): string | undefined {
|
|
|
51
51
|
cachedFromPkg = name ?? undefined;
|
|
52
52
|
return name;
|
|
53
53
|
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Resolve the app's slug — the machine-readable identifier used for the
|
|
57
|
+
* per-app transactional email sender (e.g. `clips@agent-native.com`).
|
|
58
|
+
*
|
|
59
|
+
* Only a first-party template name matched by package.json qualifies. A slug
|
|
60
|
+
* derived from an arbitrary `APP_NAME` must not mint a mailbox on the
|
|
61
|
+
* agent-native.com domain: a custom app would then send as
|
|
62
|
+
* `<its-name>@agent-native.com` and route replies to the Builder mailbox.
|
|
63
|
+
* Returns `undefined` for anything unrecognized, which leaves the deployment's
|
|
64
|
+
* configured sender in place.
|
|
65
|
+
*/
|
|
66
|
+
export function getAppSlug(): string | undefined {
|
|
67
|
+
const pkg = readPkg();
|
|
68
|
+
if (!pkg?.name) return undefined;
|
|
69
|
+
return TEMPLATES.find((t) => t.name === pkg.name)?.name;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* One-line description of the app, used to tailor transactional email bodies
|
|
74
|
+
* so recipients recognize which app they signed up for. Sourced from the
|
|
75
|
+
* first-party template metadata; `undefined` for unknown apps.
|
|
76
|
+
*/
|
|
77
|
+
export function getAppDescription(): string | undefined {
|
|
78
|
+
const pkg = readPkg();
|
|
79
|
+
if (pkg?.name) {
|
|
80
|
+
const tmpl = TEMPLATES.find((t) => t.name === pkg.name);
|
|
81
|
+
if (tmpl) return tmpl.hint;
|
|
82
|
+
}
|
|
83
|
+
return undefined;
|
|
84
|
+
}
|
|
@@ -933,11 +933,11 @@ async function createBetterAuthInstance(
|
|
|
933
933
|
""
|
|
934
934
|
).replace(/\/$/, "");
|
|
935
935
|
const resetUrl = `${appUrl}${appBasePath}/_agent-native/auth/reset?token=${encodeURIComponent(token)}`;
|
|
936
|
-
const { subject, html, text } = renderResetPasswordEmail({
|
|
936
|
+
const { subject, html, text, appSender } = renderResetPasswordEmail({
|
|
937
937
|
email: user.email,
|
|
938
938
|
resetUrl,
|
|
939
939
|
});
|
|
940
|
-
await sendEmail({ to: user.email, subject, html, text });
|
|
940
|
+
await sendEmail({ to: user.email, subject, html, text, appSender });
|
|
941
941
|
},
|
|
942
942
|
},
|
|
943
943
|
emailVerification: {
|
|
@@ -960,11 +960,11 @@ async function createBetterAuthInstance(
|
|
|
960
960
|
const verifyUrl = verifyBasePath
|
|
961
961
|
? url.replace(/(\/\/[^/]+)(\/)/, `$1${verifyBasePath}$2`)
|
|
962
962
|
: url;
|
|
963
|
-
const { subject, html, text } = renderVerifySignupEmail({
|
|
963
|
+
const { subject, html, text, appSender } = renderVerifySignupEmail({
|
|
964
964
|
email: user.email,
|
|
965
965
|
verifyUrl,
|
|
966
966
|
});
|
|
967
|
-
await sendEmail({ to: user.email, subject, html, text });
|
|
967
|
+
await sendEmail({ to: user.email, subject, html, text, appSender });
|
|
968
968
|
},
|
|
969
969
|
},
|
|
970
970
|
socialProviders,
|