@agent-native/core 0.134.0 → 0.135.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/corpus/README.md +3 -3
- package/corpus/core/CHANGELOG.md +113 -0
- package/corpus/core/docs/content/template-content-developers.mdx +1 -1
- 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-29-meeting-notes-no-longer-transcribe-the-other-side-twice-when.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/hooks/useMeetingTranscription.ts +37 -48
- package/corpus/templates/clips/desktop/src/lib/recorder.ts +331 -36
- package/corpus/templates/clips/desktop/src/lib/transcription-capture.ts +7 -5
- package/corpus/templates/clips/desktop/src/lib/transcription-engine.ts +182 -106
- package/corpus/templates/clips/desktop/src/lib/upload-recovery.ts +129 -0
- package/corpus/templates/clips/desktop/src/overlays/live-transcript.tsx +43 -14
- package/corpus/templates/clips/desktop/src/overlays/recording-pill.tsx +3 -3
- package/corpus/templates/clips/desktop/src-tauri/src/echo_guard.rs +443 -0
- package/corpus/templates/clips/desktop/src-tauri/src/lib.rs +1 -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/src-tauri/src/whisper_speech.rs +125 -16
- 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/.agents/skills/document-editing/references/databases.md +6 -4
- package/corpus/templates/content/actions/_database-membership-lock.ts +25 -0
- package/corpus/templates/content/actions/_database-row-batch.ts +6 -2
- package/corpus/templates/content/actions/_database-source-utils.ts +289 -129
- package/corpus/templates/content/actions/_database-utils.ts +12 -3
- package/corpus/templates/content/actions/add-comment.ts +15 -1
- package/corpus/templates/content/actions/add-content-database-source-field-property.ts +39 -21
- package/corpus/templates/content/actions/attach-content-database-source.ts +4 -1
- package/corpus/templates/content/actions/change-content-database-source-role.ts +3 -5
- package/corpus/templates/content/actions/duplicate-document-property.ts +59 -46
- package/corpus/templates/content/actions/execute-builder-source-execution.ts +77 -71
- package/corpus/templates/content/actions/list-documents.ts +1 -0
- package/corpus/templates/content/actions/materialize-builder-required-fields.ts +7 -0
- package/corpus/templates/content/actions/remove-database-items.ts +183 -0
- package/corpus/templates/content/actions/set-document-property.ts +57 -39
- package/corpus/templates/content/actions/sync-local-folder-source.ts +7 -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/editor/database/DatabaseView.tsx +471 -329
- package/corpus/templates/content/app/components/editor/database/row-access.ts +45 -0
- package/corpus/templates/content/app/components/editor/database/shared.tsx +141 -38
- package/corpus/templates/content/app/components/ui/switch.tsx +1 -0
- package/corpus/templates/content/app/hooks/use-content-database.ts +2 -2
- package/corpus/templates/content/app/hooks/use-content-prefs.ts +55 -0
- package/corpus/templates/content/app/i18n-data.ts +170 -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-07-30-database-rows-can-now-be-removed-without-deleting-their-page.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/parity/eval-scenarios.ts +2 -2
- package/corpus/templates/content/parity/matrix.md +1 -1
- package/corpus/templates/content/parity/matrix.ts +4 -5
- 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/api.ts +4 -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/.generated/bridge/editor-chrome.generated.ts +26 -11
- package/corpus/templates/design/actions/take-design-screenshot.ts +7 -0
- package/corpus/templates/design/app/components/design/DesignCanvas.tsx +11 -3
- package/corpus/templates/design/app/components/design/MultiScreenCanvas.tsx +4 -1
- package/corpus/templates/design/app/components/design/bridge/editor-chrome.bridge.ts +54 -17
- package/corpus/templates/design/app/components/design/design-canvas/local-runtime.ts +126 -0
- package/corpus/templates/design/app/components/templates/TemplatePreview.tsx +2 -0
- package/corpus/templates/design/app/pages/Index.tsx +3 -1
- package/corpus/templates/design/app/pages/Present.tsx +2 -1
- package/corpus/templates/design/app/routes/settings.tsx +14 -22
- package/corpus/templates/design/changelog/2026-07-30-a-broken-inline-script-in-a-generated-screen-is-now-caught-o.md +6 -0
- package/corpus/templates/design/changelog/2026-07-30-design-html-is-now-validated-with-a-spec-grade-html-parser-i.md +6 -0
- package/corpus/templates/design/changelog/2026-07-30-designs-now-load-their-tailwind-and-alpine-runtimes-from-the.md +6 -0
- package/corpus/templates/design/changelog/2026-07-30-designs-with-a-broken-alpine-expression-are-now-caught-on-sa.md +6 -0
- package/corpus/templates/design/changelog/2026-07-30-editing-a-design-no-longer-strips-its-styling-the-canvas-kep.md +6 -0
- 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/package.json +4 -0
- package/corpus/templates/design/shared/html-integrity.ts +802 -556
- 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 +2 -2
- 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/docs/content/template-content-developers.mdx +1 -1
- 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
- package/corpus/templates/content/actions/delete-database-items.ts +0 -89
|
@@ -4,14 +4,46 @@ function escapeLike(value: string): string {
|
|
|
4
4
|
return value.replace(/[!%_]/g, (match) => `!${match}`);
|
|
5
5
|
}
|
|
6
6
|
|
|
7
|
-
function chunkPrefix(
|
|
8
|
-
|
|
7
|
+
function chunkPrefix(
|
|
8
|
+
recordingId: string,
|
|
9
|
+
generationId?: string | null,
|
|
10
|
+
): string {
|
|
11
|
+
return generationId
|
|
12
|
+
? `recording-chunks-${recordingId}-${generationId}-`
|
|
13
|
+
: `recording-chunks-${recordingId}-`;
|
|
9
14
|
}
|
|
10
15
|
|
|
11
16
|
function likePrefix(prefix: string): string {
|
|
12
17
|
return `${escapeLike(prefix)}%`;
|
|
13
18
|
}
|
|
14
19
|
|
|
20
|
+
function exactChunkKeyArgs(
|
|
21
|
+
ownerEmail: string,
|
|
22
|
+
recordingId: string,
|
|
23
|
+
generationId?: string | null,
|
|
24
|
+
): [string, string, number, string, string] {
|
|
25
|
+
const prefix = chunkPrefix(recordingId, generationId);
|
|
26
|
+
return [
|
|
27
|
+
ownerEmail,
|
|
28
|
+
likePrefix(prefix),
|
|
29
|
+
prefix.length + 6,
|
|
30
|
+
`${prefix}000000`,
|
|
31
|
+
`${prefix}999999`,
|
|
32
|
+
];
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const exactChunkKeyWhere = `session_id = ? AND key LIKE ? ESCAPE '!' AND length(key) = ? AND key >= ? AND key <= ?`;
|
|
36
|
+
|
|
37
|
+
function isChunkKeyForGeneration(
|
|
38
|
+
key: string,
|
|
39
|
+
recordingId: string,
|
|
40
|
+
generationId?: string | null,
|
|
41
|
+
): boolean {
|
|
42
|
+
const prefix = chunkPrefix(recordingId, generationId);
|
|
43
|
+
if (!key.startsWith(prefix)) return false;
|
|
44
|
+
return /^\d+$/.test(key.slice(prefix.length));
|
|
45
|
+
}
|
|
46
|
+
|
|
15
47
|
function numberFromRowValue(value: unknown): number {
|
|
16
48
|
if (typeof value === "number" && Number.isFinite(value)) return value;
|
|
17
49
|
if (typeof value === "bigint") return Number(value);
|
|
@@ -81,21 +113,25 @@ export function validateRecordingChunkKeys(
|
|
|
81
113
|
export async function listRecordingChunkKeys(
|
|
82
114
|
ownerEmail: string,
|
|
83
115
|
recordingId: string,
|
|
116
|
+
generationId?: string | null,
|
|
84
117
|
): Promise<string[]> {
|
|
85
118
|
const { rows } = await getDbExec().execute({
|
|
86
|
-
sql: `SELECT key FROM application_state WHERE
|
|
87
|
-
args:
|
|
119
|
+
sql: `SELECT key FROM application_state WHERE ${exactChunkKeyWhere}`,
|
|
120
|
+
args: exactChunkKeyArgs(ownerEmail, recordingId, generationId),
|
|
88
121
|
});
|
|
89
|
-
return rows
|
|
122
|
+
return rows
|
|
123
|
+
.map((row) => String(row.key))
|
|
124
|
+
.filter((key) => isChunkKeyForGeneration(key, recordingId, generationId));
|
|
90
125
|
}
|
|
91
126
|
|
|
92
127
|
export async function deleteRecordingChunks(
|
|
93
128
|
ownerEmail: string,
|
|
94
129
|
recordingId: string,
|
|
130
|
+
generationId?: string | null,
|
|
95
131
|
): Promise<number> {
|
|
96
132
|
const result = await getDbExec().execute({
|
|
97
|
-
sql: `DELETE FROM application_state WHERE
|
|
98
|
-
args:
|
|
133
|
+
sql: `DELETE FROM application_state WHERE ${exactChunkKeyWhere}`,
|
|
134
|
+
args: exactChunkKeyArgs(ownerEmail, recordingId, generationId),
|
|
99
135
|
});
|
|
100
136
|
return result.rowsAffected ?? 0;
|
|
101
137
|
}
|
|
@@ -103,13 +139,14 @@ export async function deleteRecordingChunks(
|
|
|
103
139
|
export async function sumRecordingChunkBytes(
|
|
104
140
|
ownerEmail: string,
|
|
105
141
|
recordingId: string,
|
|
142
|
+
generationId?: string | null,
|
|
106
143
|
): Promise<number> {
|
|
107
144
|
const bytesExpression = isPostgres()
|
|
108
145
|
? `COALESCE(SUM((value::jsonb ->> 'bytes')::bigint), 0)`
|
|
109
146
|
: `COALESCE(SUM(json_extract(value, '$.bytes')), 0)`;
|
|
110
147
|
const { rows } = await getDbExec().execute({
|
|
111
|
-
sql: `SELECT ${bytesExpression} AS bytes FROM application_state WHERE
|
|
112
|
-
args:
|
|
148
|
+
sql: `SELECT ${bytesExpression} AS bytes FROM application_state WHERE ${exactChunkKeyWhere}`,
|
|
149
|
+
args: exactChunkKeyArgs(ownerEmail, recordingId, generationId),
|
|
113
150
|
});
|
|
114
151
|
return numberFromRowValue(rows[0]?.bytes);
|
|
115
152
|
}
|
|
@@ -5,9 +5,14 @@ import {
|
|
|
5
5
|
getRequestUserEmail,
|
|
6
6
|
getRequestOrgId,
|
|
7
7
|
} from "@agent-native/core/server/request-context";
|
|
8
|
+
import { getUserSetting } from "@agent-native/core/settings";
|
|
8
9
|
import { and, count, desc, eq, sql } from "drizzle-orm";
|
|
9
10
|
import { HTTPError, type H3Event } from "h3";
|
|
10
11
|
|
|
12
|
+
import {
|
|
13
|
+
CLIPS_USER_PREFS_KEY,
|
|
14
|
+
type ClipsUserPrefs,
|
|
15
|
+
} from "../../shared/clips-ai-prefs.js";
|
|
11
16
|
import { getDb, schema } from "../db/index.js";
|
|
12
17
|
|
|
13
18
|
export function getCurrentOwnerEmail(): string {
|
|
@@ -99,6 +104,25 @@ export async function getOrganizationDefaultVisibility(
|
|
|
99
104
|
}
|
|
100
105
|
}
|
|
101
106
|
|
|
107
|
+
/**
|
|
108
|
+
* Visibility for a new recording: the personal preference of the creator
|
|
109
|
+
* wins, then the organization default, then the built-in default.
|
|
110
|
+
*/
|
|
111
|
+
export async function getDefaultRecordingVisibility(
|
|
112
|
+
organizationId: string | null | undefined,
|
|
113
|
+
): Promise<RecordingVisibility> {
|
|
114
|
+
const email = getRequestUserEmail();
|
|
115
|
+
if (email) {
|
|
116
|
+
const prefs = (await getUserSetting(
|
|
117
|
+
normalizeOwnerEmail(email),
|
|
118
|
+
CLIPS_USER_PREFS_KEY,
|
|
119
|
+
)) as ClipsUserPrefs | null;
|
|
120
|
+
const preferred = prefs?.defaultRecordingVisibility;
|
|
121
|
+
if (isRecordingVisibility(preferred)) return preferred;
|
|
122
|
+
}
|
|
123
|
+
return getOrganizationDefaultVisibility(organizationId);
|
|
124
|
+
}
|
|
125
|
+
|
|
102
126
|
const ORG_ROLE_RANK: Record<OrganizationAccessRole, number> = {
|
|
103
127
|
member: 1,
|
|
104
128
|
admin: 2,
|
|
@@ -12,12 +12,16 @@ export interface StoredResumableSession {
|
|
|
12
12
|
lastCommittedIndex?: number;
|
|
13
13
|
}
|
|
14
14
|
|
|
15
|
-
const key = (recordingId: string) =>
|
|
15
|
+
const key = (recordingId: string, generationId?: string | null) =>
|
|
16
|
+
generationId
|
|
17
|
+
? `resumable-session-${recordingId}-${generationId}`
|
|
18
|
+
: `resumable-session-${recordingId}`;
|
|
16
19
|
|
|
17
20
|
export async function getResumableSession(
|
|
18
21
|
recordingId: string,
|
|
22
|
+
generationId?: string | null,
|
|
19
23
|
): Promise<StoredResumableSession | null> {
|
|
20
|
-
const raw = await readAppState(key(recordingId));
|
|
24
|
+
const raw = await readAppState(key(recordingId, generationId));
|
|
21
25
|
if (!raw || typeof raw !== "object") return null;
|
|
22
26
|
return raw as unknown as StoredResumableSession;
|
|
23
27
|
}
|
|
@@ -25,15 +29,17 @@ export async function getResumableSession(
|
|
|
25
29
|
export async function setResumableSession(
|
|
26
30
|
recordingId: string,
|
|
27
31
|
session: StoredResumableSession,
|
|
32
|
+
generationId?: string | null,
|
|
28
33
|
): Promise<void> {
|
|
29
34
|
await writeAppState(
|
|
30
|
-
key(recordingId),
|
|
35
|
+
key(recordingId, generationId),
|
|
31
36
|
session as unknown as Record<string, unknown>,
|
|
32
37
|
);
|
|
33
38
|
}
|
|
34
39
|
|
|
35
40
|
export async function deleteResumableSession(
|
|
36
41
|
recordingId: string,
|
|
42
|
+
generationId?: string | null,
|
|
37
43
|
): Promise<void> {
|
|
38
|
-
await deleteAppState(key(recordingId));
|
|
44
|
+
await deleteAppState(key(recordingId, generationId));
|
|
39
45
|
}
|
|
@@ -9,6 +9,8 @@ const CLIPS_BRAND_NAME = "Clips";
|
|
|
9
9
|
const EMAIL_SEND_TIMEOUT_MS = 60_000;
|
|
10
10
|
const FRIENDLY_REPLY_TO = "hello@agent-native.com";
|
|
11
11
|
const UNTITLED_CLIP = "Untitled Clip";
|
|
12
|
+
const ACTIVITY_EMAIL_FOOTER =
|
|
13
|
+
"You received this because email notifications are on in your Clips settings.";
|
|
12
14
|
|
|
13
15
|
interface TransactionalEmailBase {
|
|
14
16
|
to: string;
|
|
@@ -37,6 +39,25 @@ export type ClipsTransactionalEmailInput =
|
|
|
37
39
|
| (TransactionalEmailBase & {
|
|
38
40
|
kind: "two-clips";
|
|
39
41
|
generatedSummary?: string | null;
|
|
42
|
+
})
|
|
43
|
+
| (TransactionalEmailBase & {
|
|
44
|
+
kind: "activity-comment";
|
|
45
|
+
recordingId: string;
|
|
46
|
+
title?: string | null;
|
|
47
|
+
authorEmail?: string | null;
|
|
48
|
+
authorName?: string | null;
|
|
49
|
+
content: string;
|
|
50
|
+
videoTimestampMs?: number | null;
|
|
51
|
+
isReply?: boolean;
|
|
52
|
+
})
|
|
53
|
+
| (TransactionalEmailBase & {
|
|
54
|
+
kind: "activity-reaction";
|
|
55
|
+
recordingId: string;
|
|
56
|
+
title?: string | null;
|
|
57
|
+
emoji: string;
|
|
58
|
+
authorEmail?: string | null;
|
|
59
|
+
authorName?: string | null;
|
|
60
|
+
videoTimestampMs?: number | null;
|
|
40
61
|
});
|
|
41
62
|
|
|
42
63
|
export interface ClipsTransactionalEmailRenderOptions {
|
|
@@ -103,6 +124,31 @@ function clipUrl(
|
|
|
103
124
|
return appUrlForPath(`/r/${encodeURIComponent(recordingId)}`, options);
|
|
104
125
|
}
|
|
105
126
|
|
|
127
|
+
function clipCommentsUrl(
|
|
128
|
+
recordingId: string,
|
|
129
|
+
videoTimestampMs: number | null | undefined,
|
|
130
|
+
options: ClipsTransactionalEmailRenderOptions,
|
|
131
|
+
): string {
|
|
132
|
+
const url = new URL(clipUrl(recordingId, options));
|
|
133
|
+
url.searchParams.set("panel", "comments");
|
|
134
|
+
if (typeof videoTimestampMs === "number" && videoTimestampMs > 0) {
|
|
135
|
+
url.searchParams.set("t", String(Math.floor(videoTimestampMs / 1000)));
|
|
136
|
+
}
|
|
137
|
+
return url.toString();
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function formatTimestamp(ms: number): string {
|
|
141
|
+
const total = Math.floor(ms / 1000);
|
|
142
|
+
const minutes = Math.floor(total / 60);
|
|
143
|
+
const seconds = total % 60;
|
|
144
|
+
return `${minutes}:${String(seconds).padStart(2, "0")}`;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function quotedExcerpt(value: string): string {
|
|
148
|
+
const text = singleLine(value);
|
|
149
|
+
return text.length > 280 ? `${text.slice(0, 277)}…` : text;
|
|
150
|
+
}
|
|
151
|
+
|
|
106
152
|
function recordUrl(options: ClipsTransactionalEmailRenderOptions): string {
|
|
107
153
|
return appUrlForPath("/record", options);
|
|
108
154
|
}
|
|
@@ -232,6 +278,69 @@ export function renderClipsTransactionalEmail(
|
|
|
232
278
|
});
|
|
233
279
|
return { subject, ...rendered };
|
|
234
280
|
}
|
|
281
|
+
|
|
282
|
+
case "activity-comment": {
|
|
283
|
+
const author =
|
|
284
|
+
singleLine(input.authorName) ||
|
|
285
|
+
normalizeEmailDisplayName(input.authorEmail, "Someone");
|
|
286
|
+
const at =
|
|
287
|
+
typeof input.videoTimestampMs === "number" && input.videoTimestampMs > 0
|
|
288
|
+
? ` at ${formatTimestamp(input.videoTimestampMs)}`
|
|
289
|
+
: "";
|
|
290
|
+
const subject = input.isReply
|
|
291
|
+
? `${author} replied on “${title}”`
|
|
292
|
+
: `${author} commented on “${title}”`;
|
|
293
|
+
const rendered = renderEmail({
|
|
294
|
+
brandName: CLIPS_BRAND_NAME,
|
|
295
|
+
preheader: subject,
|
|
296
|
+
heading: input.isReply
|
|
297
|
+
? `${author} replied on your Clip`
|
|
298
|
+
: `${author} commented on your Clip`,
|
|
299
|
+
paragraphs: [
|
|
300
|
+
`${emailStrong(author)} left a ${input.isReply ? "reply" : "comment"} on ${emailStrong(title!)}${at}.`,
|
|
301
|
+
`“${quotedExcerpt(input.content)}”`,
|
|
302
|
+
],
|
|
303
|
+
cta: {
|
|
304
|
+
label: "Read and reply",
|
|
305
|
+
url: clipCommentsUrl(
|
|
306
|
+
input.recordingId,
|
|
307
|
+
input.videoTimestampMs,
|
|
308
|
+
options,
|
|
309
|
+
),
|
|
310
|
+
},
|
|
311
|
+
footer: ACTIVITY_EMAIL_FOOTER,
|
|
312
|
+
});
|
|
313
|
+
return { subject, ...rendered };
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
case "activity-reaction": {
|
|
317
|
+
const author =
|
|
318
|
+
singleLine(input.authorName) ||
|
|
319
|
+
normalizeEmailDisplayName(input.authorEmail, "Someone");
|
|
320
|
+
const at =
|
|
321
|
+
typeof input.videoTimestampMs === "number" && input.videoTimestampMs > 0
|
|
322
|
+
? ` at ${formatTimestamp(input.videoTimestampMs)}`
|
|
323
|
+
: "";
|
|
324
|
+
const subject = `${author} reacted ${input.emoji} on “${title}”`;
|
|
325
|
+
const rendered = renderEmail({
|
|
326
|
+
brandName: CLIPS_BRAND_NAME,
|
|
327
|
+
preheader: subject,
|
|
328
|
+
heading: "Someone reacted to your Clip",
|
|
329
|
+
paragraphs: [
|
|
330
|
+
`${emailStrong(author)} reacted ${emailStrong(input.emoji)} on ${emailStrong(title!)}${at}.`,
|
|
331
|
+
],
|
|
332
|
+
cta: {
|
|
333
|
+
label: "See Clip activity",
|
|
334
|
+
url: clipCommentsUrl(
|
|
335
|
+
input.recordingId,
|
|
336
|
+
input.videoTimestampMs,
|
|
337
|
+
options,
|
|
338
|
+
),
|
|
339
|
+
},
|
|
340
|
+
footer: ACTIVITY_EMAIL_FOOTER,
|
|
341
|
+
});
|
|
342
|
+
return { subject, ...rendered };
|
|
343
|
+
}
|
|
235
344
|
}
|
|
236
345
|
}
|
|
237
346
|
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export const RETRYABLE_UPLOAD_INTERRUPTION_REASON =
|
|
2
|
+
"Upload was interrupted. The local recording is safe; retry from the Clips desktop app.";
|
|
3
|
+
|
|
4
|
+
export function isRetryableUploadInterruption(
|
|
5
|
+
failureReason: string | null | undefined,
|
|
6
|
+
): boolean {
|
|
7
|
+
return failureReason === RETRYABLE_UPLOAD_INTERRUPTION_REASON;
|
|
8
|
+
}
|
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
14
|
import { getDbExec, isPostgres } from "@agent-native/core/db";
|
|
15
|
-
import { and, eq, inArray, sql } from "drizzle-orm";
|
|
15
|
+
import { and, eq, inArray, isNull, sql } from "drizzle-orm";
|
|
16
16
|
|
|
17
17
|
import { getDb, schema } from "../db/index.js";
|
|
18
18
|
|
|
@@ -37,6 +37,7 @@ export type UploadLeaseResult =
|
|
|
37
37
|
| { held: true }
|
|
38
38
|
| {
|
|
39
39
|
held: false;
|
|
40
|
+
staleAttempt: boolean;
|
|
40
41
|
status: string | null;
|
|
41
42
|
failureReason: string | null;
|
|
42
43
|
videoUrl: string | null;
|
|
@@ -53,7 +54,12 @@ export type UploadLeaseResult =
|
|
|
53
54
|
*/
|
|
54
55
|
export async function renewUploadLease(
|
|
55
56
|
recordingId: string,
|
|
56
|
-
options: {
|
|
57
|
+
options: {
|
|
58
|
+
now?: number;
|
|
59
|
+
uploadProgress?: number;
|
|
60
|
+
attemptId?: string | null;
|
|
61
|
+
generationId?: string | null;
|
|
62
|
+
} = {},
|
|
57
63
|
): Promise<UploadLeaseResult> {
|
|
58
64
|
const now = options.now ?? Date.now();
|
|
59
65
|
const held = await getDb()
|
|
@@ -72,6 +78,23 @@ export async function renewUploadLease(
|
|
|
72
78
|
and(
|
|
73
79
|
eq(schema.recordings.id, recordingId),
|
|
74
80
|
inArray(schema.recordings.status, [...IN_PROGRESS_STATUSES]),
|
|
81
|
+
...(options.attemptId === undefined
|
|
82
|
+
? []
|
|
83
|
+
: [
|
|
84
|
+
options.attemptId === null
|
|
85
|
+
? isNull(schema.recordings.uploadAttemptId)
|
|
86
|
+
: eq(schema.recordings.uploadAttemptId, options.attemptId),
|
|
87
|
+
]),
|
|
88
|
+
...(options.generationId === undefined
|
|
89
|
+
? []
|
|
90
|
+
: [
|
|
91
|
+
options.generationId === null
|
|
92
|
+
? isNull(schema.recordings.uploadGenerationId)
|
|
93
|
+
: eq(
|
|
94
|
+
schema.recordings.uploadGenerationId,
|
|
95
|
+
options.generationId,
|
|
96
|
+
),
|
|
97
|
+
]),
|
|
75
98
|
),
|
|
76
99
|
)
|
|
77
100
|
.returning({ id: schema.recordings.id });
|
|
@@ -87,12 +110,23 @@ export async function renewUploadLease(
|
|
|
87
110
|
videoUrl: schema.recordings.videoUrl,
|
|
88
111
|
videoSizeBytes: schema.recordings.videoSizeBytes,
|
|
89
112
|
durationMs: schema.recordings.durationMs,
|
|
113
|
+
uploadAttemptId: schema.recordings.uploadAttemptId,
|
|
114
|
+
uploadGenerationId: schema.recordings.uploadGenerationId,
|
|
90
115
|
})
|
|
91
116
|
.from(schema.recordings)
|
|
92
117
|
.where(eq(schema.recordings.id, recordingId));
|
|
93
118
|
|
|
94
119
|
return {
|
|
95
120
|
held: false,
|
|
121
|
+
staleAttempt:
|
|
122
|
+
(options.attemptId !== undefined || options.generationId !== undefined) &&
|
|
123
|
+
IN_PROGRESS_STATUSES.includes(
|
|
124
|
+
row?.status as (typeof IN_PROGRESS_STATUSES)[number],
|
|
125
|
+
) &&
|
|
126
|
+
((options.attemptId !== undefined &&
|
|
127
|
+
row?.uploadAttemptId !== options.attemptId) ||
|
|
128
|
+
(options.generationId !== undefined &&
|
|
129
|
+
row?.uploadGenerationId !== options.generationId)),
|
|
96
130
|
status: row?.status ?? null,
|
|
97
131
|
failureReason: row?.failureReason ?? null,
|
|
98
132
|
videoUrl: row?.videoUrl ?? null,
|
|
@@ -106,6 +140,7 @@ export interface ReapedUpload {
|
|
|
106
140
|
ownerEmail: string;
|
|
107
141
|
status: string;
|
|
108
142
|
leaseExpiresAt: string;
|
|
143
|
+
uploadGenerationId: string | null;
|
|
109
144
|
}
|
|
110
145
|
|
|
111
146
|
export interface ReapResult {
|
|
@@ -137,7 +172,7 @@ export async function reapExpiredUploads(
|
|
|
137
172
|
|
|
138
173
|
// guard:allow-unscoped — system upload reaper, owner-agnostic by design.
|
|
139
174
|
const probe = await exec.execute({
|
|
140
|
-
sql: `SELECT id, owner_email, status, upload_lease_expires_at
|
|
175
|
+
sql: `SELECT id, owner_email, status, upload_lease_expires_at, upload_generation_id
|
|
141
176
|
FROM recordings
|
|
142
177
|
WHERE status IN ('uploading', 'processing')
|
|
143
178
|
AND upload_lease_expires_at IS NOT NULL
|
|
@@ -154,6 +189,10 @@ export async function reapExpiredUploads(
|
|
|
154
189
|
ownerEmail: String(row.owner_email ?? ""),
|
|
155
190
|
status: String(row.status ?? ""),
|
|
156
191
|
leaseExpiresAt: String(row.upload_lease_expires_at ?? ""),
|
|
192
|
+
uploadGenerationId:
|
|
193
|
+
typeof row.upload_generation_id === "string"
|
|
194
|
+
? row.upload_generation_id
|
|
195
|
+
: null,
|
|
157
196
|
}));
|
|
158
197
|
|
|
159
198
|
let failed = 0;
|
|
@@ -184,9 +223,16 @@ export async function reapExpiredUploads(
|
|
|
184
223
|
failed = terminated.size;
|
|
185
224
|
|
|
186
225
|
for (const id of terminated) {
|
|
226
|
+
const generationId = expired.find(
|
|
227
|
+
(row) => row.id === id,
|
|
228
|
+
)?.uploadGenerationId;
|
|
187
229
|
await exec.execute({
|
|
188
230
|
sql: `DELETE FROM application_state WHERE key = ${p(1)}`,
|
|
189
|
-
args: [
|
|
231
|
+
args: [
|
|
232
|
+
generationId
|
|
233
|
+
? `resumable-session-${id}-${generationId}`
|
|
234
|
+
: `resumable-session-${id}`,
|
|
235
|
+
],
|
|
190
236
|
});
|
|
191
237
|
}
|
|
192
238
|
}
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Generate a filmstrip sprite — one JPEG holding a grid of evenly-spaced video
|
|
3
|
+
* frames — so the editor timeline renders thumbnails as a single cached image
|
|
4
|
+
* request instead of decoding the video in the browser.
|
|
5
|
+
*
|
|
6
|
+
* Why a sprite rather than per-frame files: one ffmpeg pass, one upload, one
|
|
7
|
+
* HTTP request, and the grid geometry is enough for CSS `background-position`
|
|
8
|
+
* to address any cell. The browser fallback in `app/lib/video-filmstrip.ts`
|
|
9
|
+
* needs one seek per frame and cannot read cross-origin media at all.
|
|
10
|
+
*
|
|
11
|
+
* Three constraints worth keeping in mind:
|
|
12
|
+
*
|
|
13
|
+
* - Cells are padded to an exact size (`force_original_aspect_ratio=decrease`
|
|
14
|
+
* plus `pad`) so the returned grid geometry is exact without probing the
|
|
15
|
+
* source. Letterboxed bars on odd aspect ratios are the deliberate cost of
|
|
16
|
+
* not having to decode the output to learn its dimensions.
|
|
17
|
+
* - Sampling starts half a cell in, matching the browser fallback: cell `i`
|
|
18
|
+
* shows the midpoint of the time slot it occupies, not its leading edge.
|
|
19
|
+
* - Input arrives as bytes and is written to a temp file, like every other
|
|
20
|
+
* ffmpeg path here. That means the whole file is buffered in memory by the
|
|
21
|
+
* caller, which is the practical ceiling on this approach for long
|
|
22
|
+
* recordings on small hosts.
|
|
23
|
+
*/
|
|
24
|
+
|
|
25
|
+
import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
|
|
26
|
+
import { tmpdir } from "node:os";
|
|
27
|
+
import { join } from "node:path";
|
|
28
|
+
|
|
29
|
+
import { isFfmpegAvailable, runFfmpeg, withRemuxSlot } from "./video-remux.js";
|
|
30
|
+
|
|
31
|
+
const SPRITE_TIMEOUT_MS = 90_000;
|
|
32
|
+
|
|
33
|
+
export const DEFAULT_FILMSTRIP_FRAME_COUNT = 40;
|
|
34
|
+
// A sprite is decoded whole by the browser, so cap the grid: 120 cells at the
|
|
35
|
+
// default size is already a ~1600x1080 image.
|
|
36
|
+
export const MAX_FILMSTRIP_FRAME_COUNT = 120;
|
|
37
|
+
export const DEFAULT_FILMSTRIP_FRAME_WIDTH = 160;
|
|
38
|
+
export const DEFAULT_FILMSTRIP_FRAME_HEIGHT = 90;
|
|
39
|
+
export const DEFAULT_FILMSTRIP_COLUMNS = 10;
|
|
40
|
+
|
|
41
|
+
export type FilmstripSpriteStatus =
|
|
42
|
+
| "generated"
|
|
43
|
+
| "skipped-no-ffmpeg"
|
|
44
|
+
| "skipped-no-duration"
|
|
45
|
+
| "skipped-no-media"
|
|
46
|
+
| "failed-ffmpeg"
|
|
47
|
+
| "failed-empty-output";
|
|
48
|
+
|
|
49
|
+
/** Everything the UI needs to address one cell of the sprite. */
|
|
50
|
+
export interface FilmstripSpriteGrid {
|
|
51
|
+
frameCount: number;
|
|
52
|
+
columns: number;
|
|
53
|
+
rows: number;
|
|
54
|
+
frameWidth: number;
|
|
55
|
+
frameHeight: number;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export interface FilmstripSpriteResult {
|
|
59
|
+
status: FilmstripSpriteStatus;
|
|
60
|
+
/** Present only when `status === "generated"`. */
|
|
61
|
+
sprite?: { bytes: Uint8Array; grid: FilmstripSpriteGrid };
|
|
62
|
+
detail?: string;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export interface GenerateFilmstripSpriteInput {
|
|
66
|
+
mediaBytes: Uint8Array;
|
|
67
|
+
durationMs: number;
|
|
68
|
+
frameCount?: number;
|
|
69
|
+
frameWidth?: number;
|
|
70
|
+
frameHeight?: number;
|
|
71
|
+
columns?: number;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Lay `frameCount` cells out into a grid no wider than `columns`. Exported so
|
|
76
|
+
* callers can compute the same geometry the sprite will have without
|
|
77
|
+
* regenerating it.
|
|
78
|
+
*/
|
|
79
|
+
export function filmstripGrid(input: {
|
|
80
|
+
frameCount: number;
|
|
81
|
+
columns: number;
|
|
82
|
+
frameWidth: number;
|
|
83
|
+
frameHeight: number;
|
|
84
|
+
}): FilmstripSpriteGrid {
|
|
85
|
+
const frameCount = Math.min(
|
|
86
|
+
MAX_FILMSTRIP_FRAME_COUNT,
|
|
87
|
+
Math.max(1, Math.floor(input.frameCount)),
|
|
88
|
+
);
|
|
89
|
+
const columns = Math.max(1, Math.min(input.columns, frameCount));
|
|
90
|
+
return {
|
|
91
|
+
frameCount,
|
|
92
|
+
columns,
|
|
93
|
+
rows: Math.ceil(frameCount / columns),
|
|
94
|
+
frameWidth: input.frameWidth,
|
|
95
|
+
frameHeight: input.frameHeight,
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Build the ffmpeg filter chain that samples `frameCount` frames across
|
|
101
|
+
* `durationMs` and tiles them into `grid`. Exported for tests: the sampling
|
|
102
|
+
* offset and rate are the parts most likely to drift from the browser
|
|
103
|
+
* fallback's expectations.
|
|
104
|
+
*/
|
|
105
|
+
export function filmstripSpriteFilter(input: {
|
|
106
|
+
durationMs: number;
|
|
107
|
+
grid: FilmstripSpriteGrid;
|
|
108
|
+
}): { seekSeconds: string; filter: string } {
|
|
109
|
+
const { durationMs, grid } = input;
|
|
110
|
+
const cellMs = durationMs / grid.frameCount;
|
|
111
|
+
const fps = grid.frameCount / (durationMs / 1000);
|
|
112
|
+
|
|
113
|
+
return {
|
|
114
|
+
// Half a cell in, so each tile is the midpoint of its slot.
|
|
115
|
+
seekSeconds: (cellMs / 2 / 1000).toFixed(6),
|
|
116
|
+
filter: [
|
|
117
|
+
`fps=${fps.toFixed(6)}`,
|
|
118
|
+
`scale=${grid.frameWidth}:${grid.frameHeight}:force_original_aspect_ratio=decrease`,
|
|
119
|
+
`pad=${grid.frameWidth}:${grid.frameHeight}:-1:-1:color=black`,
|
|
120
|
+
`tile=${grid.columns}x${grid.rows}`,
|
|
121
|
+
].join(","),
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export async function generateFilmstripSprite(
|
|
126
|
+
input: GenerateFilmstripSpriteInput,
|
|
127
|
+
): Promise<FilmstripSpriteResult> {
|
|
128
|
+
const {
|
|
129
|
+
mediaBytes,
|
|
130
|
+
durationMs,
|
|
131
|
+
frameCount = DEFAULT_FILMSTRIP_FRAME_COUNT,
|
|
132
|
+
frameWidth = DEFAULT_FILMSTRIP_FRAME_WIDTH,
|
|
133
|
+
frameHeight = DEFAULT_FILMSTRIP_FRAME_HEIGHT,
|
|
134
|
+
columns = DEFAULT_FILMSTRIP_COLUMNS,
|
|
135
|
+
} = input;
|
|
136
|
+
|
|
137
|
+
if (mediaBytes.byteLength === 0) {
|
|
138
|
+
return { status: "skipped-no-media" };
|
|
139
|
+
}
|
|
140
|
+
if (!Number.isFinite(durationMs) || durationMs <= 0) {
|
|
141
|
+
return { status: "skipped-no-duration" };
|
|
142
|
+
}
|
|
143
|
+
if (!isFfmpegAvailable()) {
|
|
144
|
+
return {
|
|
145
|
+
status: "skipped-no-ffmpeg",
|
|
146
|
+
detail:
|
|
147
|
+
"No ffmpeg binary available; the editor falls back to browser-side frame extraction.",
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
const grid = filmstripGrid({ frameCount, columns, frameWidth, frameHeight });
|
|
152
|
+
const { seekSeconds, filter } = filmstripSpriteFilter({ durationMs, grid });
|
|
153
|
+
|
|
154
|
+
const dir = await mkdtemp(join(tmpdir(), "clips-filmstrip-"));
|
|
155
|
+
const inputPath = join(dir, "input.media");
|
|
156
|
+
const outputPath = join(dir, "sprite.jpg");
|
|
157
|
+
|
|
158
|
+
try {
|
|
159
|
+
await writeFile(inputPath, mediaBytes);
|
|
160
|
+
await withRemuxSlot(() =>
|
|
161
|
+
runFfmpeg(
|
|
162
|
+
[
|
|
163
|
+
"-hide_banner",
|
|
164
|
+
"-loglevel",
|
|
165
|
+
"error",
|
|
166
|
+
"-nostdin",
|
|
167
|
+
"-y",
|
|
168
|
+
// Input seeking: cheap, and puts the first sample at a cell midpoint.
|
|
169
|
+
"-ss",
|
|
170
|
+
seekSeconds,
|
|
171
|
+
"-i",
|
|
172
|
+
inputPath,
|
|
173
|
+
"-an",
|
|
174
|
+
"-sn",
|
|
175
|
+
"-vf",
|
|
176
|
+
filter,
|
|
177
|
+
// The tile filter emits the finished grid as a single frame.
|
|
178
|
+
"-frames:v",
|
|
179
|
+
"1",
|
|
180
|
+
"-q:v",
|
|
181
|
+
"4",
|
|
182
|
+
"-f",
|
|
183
|
+
"image2",
|
|
184
|
+
outputPath,
|
|
185
|
+
],
|
|
186
|
+
{ timeoutMs: SPRITE_TIMEOUT_MS, label: "filmstrip sprite" },
|
|
187
|
+
),
|
|
188
|
+
);
|
|
189
|
+
|
|
190
|
+
const bytes = new Uint8Array(await readFile(outputPath));
|
|
191
|
+
if (bytes.byteLength === 0) {
|
|
192
|
+
return {
|
|
193
|
+
status: "failed-empty-output",
|
|
194
|
+
detail: "ffmpeg reported success but wrote an empty sprite",
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
return { status: "generated", sprite: { bytes, grid } };
|
|
199
|
+
} catch (err) {
|
|
200
|
+
return {
|
|
201
|
+
status: "failed-ffmpeg",
|
|
202
|
+
detail: err instanceof Error ? err.message : String(err),
|
|
203
|
+
};
|
|
204
|
+
} finally {
|
|
205
|
+
await rm(dir, { recursive: true, force: true }).catch(() => {});
|
|
206
|
+
}
|
|
207
|
+
}
|
|
@@ -125,7 +125,11 @@ function startsWithMagic(bytes: Uint8Array, magic: number[]): boolean {
|
|
|
125
125
|
return true;
|
|
126
126
|
}
|
|
127
127
|
|
|
128
|
-
async function runFfmpeg(
|
|
128
|
+
export async function runFfmpeg(
|
|
129
|
+
args: string[],
|
|
130
|
+
options: { timeoutMs?: number; label?: string } = {},
|
|
131
|
+
): Promise<void> {
|
|
132
|
+
const { timeoutMs = REMUX_TIMEOUT_MS, label = "remux" } = options;
|
|
129
133
|
await new Promise<void>((resolve, reject) => {
|
|
130
134
|
const child = spawn(ffmpegCommand(), args, {
|
|
131
135
|
stdio: ["ignore", "ignore", "pipe"],
|
|
@@ -133,8 +137,8 @@ async function runFfmpeg(args: string[]): Promise<void> {
|
|
|
133
137
|
let stderr = "";
|
|
134
138
|
const timeout = setTimeout(() => {
|
|
135
139
|
child.kill("SIGKILL");
|
|
136
|
-
reject(new Error(`ffmpeg
|
|
137
|
-
},
|
|
140
|
+
reject(new Error(`ffmpeg ${label} timed out\n${stderr}`));
|
|
141
|
+
}, timeoutMs);
|
|
138
142
|
|
|
139
143
|
child.stderr?.on("data", (chunk: Buffer) => {
|
|
140
144
|
stderr = (stderr + chunk.toString("utf8")).slice(-STDERR_LIMIT);
|
|
@@ -244,7 +248,12 @@ export async function probeHasAudioStream(
|
|
|
244
248
|
}
|
|
245
249
|
}
|
|
246
250
|
|
|
247
|
-
|
|
251
|
+
/**
|
|
252
|
+
* Bound concurrent ffmpeg invocations process-wide. Every ffmpeg caller must go
|
|
253
|
+
* through this, including non-remux jobs — the limit protects the box, not the
|
|
254
|
+
* remux path specifically.
|
|
255
|
+
*/
|
|
256
|
+
export async function withRemuxSlot<T>(fn: () => Promise<T>): Promise<T> {
|
|
248
257
|
if (activeRemuxes >= MAX_CONCURRENT_REMUXES) {
|
|
249
258
|
await new Promise<void>((resolve) => remuxWaiters.push(resolve));
|
|
250
259
|
}
|