@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.
- package/corpus/README.md +3 -3
- package/corpus/core/CHANGELOG.md +113 -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/access.ts +44 -2
- 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/_database-utils.ts +113 -4
- package/corpus/templates/content/actions/add-comment.ts +15 -1
- package/corpus/templates/content/actions/get-document.ts +84 -3
- package/corpus/templates/content/actions/list-documents.ts +1 -0
- package/corpus/templates/content/app/components/editor/BuilderBodySyncingNotice.tsx +9 -2
- package/corpus/templates/content/app/components/editor/DocumentEditor.tsx +34 -15
- package/corpus/templates/content/app/components/editor/DocumentToolbar.tsx +55 -13
- package/corpus/templates/content/app/components/editor/body-hydration.ts +12 -6
- package/corpus/templates/content/app/components/editor/database/DatabaseView.tsx +2 -3
- package/corpus/templates/content/app/components/sidebar/DocumentSidebar.tsx +5 -12
- package/corpus/templates/content/app/components/ui/switch.tsx +1 -0
- package/corpus/templates/content/app/hooks/use-content-database.ts +16 -27
- package/corpus/templates/content/app/hooks/use-content-prefs.ts +55 -0
- package/corpus/templates/content/app/hooks/use-create-page.ts +3 -6
- package/corpus/templates/content/app/hooks/use-document-properties.ts +9 -16
- package/corpus/templates/content/app/hooks/use-document-versions.ts +3 -3
- package/corpus/templates/content/app/hooks/use-documents.ts +85 -29
- package/corpus/templates/content/app/hooks/use-notion.ts +15 -13
- package/corpus/templates/content/app/i18n-data.ts +72 -1
- package/corpus/templates/content/app/lib/document-query.ts +36 -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/changelog/2026-08-02-pages-opened-from-a-database-now-keep-that-database-s-fields.md +6 -0
- package/corpus/templates/content/server/lib/comment-notifications.ts +146 -0
- package/corpus/templates/content/server/lib/document-context.ts +11 -6
- 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/api.ts +8 -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/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 +4 -4
- package/dist/observability/routes.d.ts +3 -3
- 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/progress/routes.d.ts +1 -1
- package/dist/provider-api/actions/custom-provider-registration.d.ts +13 -13
- package/dist/provider-api/actions/provider-api.d.ts +8 -8
- package/dist/provider-api/corpus-jobs.d.ts +2 -2
- 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/server/transcribe-voice.d.ts +1 -1
- package/dist/sharing/access.d.ts.map +1 -1
- package/dist/sharing/access.js +32 -2
- package/dist/sharing/access.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/access.ts +44 -2
- 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
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 {
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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<
|
|
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 {
|
|
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
|
-
|
|
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
|
|
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<
|
|
30
|
+
run: async (args, ctx): Promise<ResolvedLocalizationPreference> => {
|
|
21
31
|
if (!ctx?.userEmail) throw new Error("Not authenticated.");
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
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
|
-
|
|
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
|
-
):
|
|
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.
|
package/src/org/index.ts
CHANGED
|
@@ -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) => {
|
package/src/review/index.ts
CHANGED
|
@@ -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
|
+
}
|
package/src/review/types.ts
CHANGED
|
@@ -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,
|