diff --git a/packages/twenty-apps/internal/call-recorder/.env.example b/packages/twenty-apps/public/call-recorder/.env.example similarity index 100% rename from packages/twenty-apps/internal/call-recorder/.env.example rename to packages/twenty-apps/public/call-recorder/.env.example diff --git a/packages/twenty-apps/internal/call-recorder/.gitignore b/packages/twenty-apps/public/call-recorder/.gitignore similarity index 100% rename from packages/twenty-apps/internal/call-recorder/.gitignore rename to packages/twenty-apps/public/call-recorder/.gitignore diff --git a/packages/twenty-apps/internal/call-recorder/.nvmrc b/packages/twenty-apps/public/call-recorder/.nvmrc similarity index 100% rename from packages/twenty-apps/internal/call-recorder/.nvmrc rename to packages/twenty-apps/public/call-recorder/.nvmrc diff --git a/packages/twenty-apps/internal/call-recorder/.oxlintrc.json b/packages/twenty-apps/public/call-recorder/.oxlintrc.json similarity index 100% rename from packages/twenty-apps/internal/call-recorder/.oxlintrc.json rename to packages/twenty-apps/public/call-recorder/.oxlintrc.json diff --git a/packages/twenty-apps/internal/call-recorder/.yarnrc.yml b/packages/twenty-apps/public/call-recorder/.yarnrc.yml similarity index 100% rename from packages/twenty-apps/internal/call-recorder/.yarnrc.yml rename to packages/twenty-apps/public/call-recorder/.yarnrc.yml diff --git a/packages/twenty-apps/internal/call-recorder/AGENTS.md b/packages/twenty-apps/public/call-recorder/AGENTS.md similarity index 100% rename from packages/twenty-apps/internal/call-recorder/AGENTS.md rename to packages/twenty-apps/public/call-recorder/AGENTS.md diff --git a/packages/twenty-apps/internal/call-recorder/CLAUDE.md b/packages/twenty-apps/public/call-recorder/CLAUDE.md similarity index 100% rename from packages/twenty-apps/internal/call-recorder/CLAUDE.md rename to packages/twenty-apps/public/call-recorder/CLAUDE.md diff --git a/packages/twenty-apps/internal/call-recorder/README.md b/packages/twenty-apps/public/call-recorder/README.md similarity index 100% rename from packages/twenty-apps/internal/call-recorder/README.md rename to packages/twenty-apps/public/call-recorder/README.md diff --git a/packages/twenty-apps/internal/call-recorder/package.json b/packages/twenty-apps/public/call-recorder/package.json similarity index 100% rename from packages/twenty-apps/internal/call-recorder/package.json rename to packages/twenty-apps/public/call-recorder/package.json diff --git a/packages/twenty-apps/internal/call-recorder/public/gallery/call-recorder-cover.png b/packages/twenty-apps/public/call-recorder/public/gallery/call-recorder-cover.png similarity index 100% rename from packages/twenty-apps/internal/call-recorder/public/gallery/call-recorder-cover.png rename to packages/twenty-apps/public/call-recorder/public/gallery/call-recorder-cover.png diff --git a/packages/twenty-apps/internal/call-recorder/public/logo.svg b/packages/twenty-apps/public/call-recorder/public/logo.svg similarity index 100% rename from packages/twenty-apps/internal/call-recorder/public/logo.svg rename to packages/twenty-apps/public/call-recorder/public/logo.svg diff --git a/packages/twenty-apps/internal/call-recorder/src/__tests__/global-setup.ts b/packages/twenty-apps/public/call-recorder/src/__tests__/global-setup.ts similarity index 100% rename from packages/twenty-apps/internal/call-recorder/src/__tests__/global-setup.ts rename to packages/twenty-apps/public/call-recorder/src/__tests__/global-setup.ts diff --git a/packages/twenty-apps/internal/call-recorder/src/__tests__/schema.integration-test.ts b/packages/twenty-apps/public/call-recorder/src/__tests__/schema.integration-test.ts similarity index 100% rename from packages/twenty-apps/internal/call-recorder/src/__tests__/schema.integration-test.ts rename to packages/twenty-apps/public/call-recorder/src/__tests__/schema.integration-test.ts diff --git a/packages/twenty-apps/internal/call-recorder/src/application-config.ts b/packages/twenty-apps/public/call-recorder/src/application-config.ts similarity index 100% rename from packages/twenty-apps/internal/call-recorder/src/application-config.ts rename to packages/twenty-apps/public/call-recorder/src/application-config.ts diff --git a/packages/twenty-apps/internal/call-recorder/src/constants/__tests__/call-recording-field-universal-identifiers.test.ts b/packages/twenty-apps/public/call-recorder/src/constants/__tests__/call-recording-field-universal-identifiers.test.ts similarity index 100% rename from packages/twenty-apps/internal/call-recorder/src/constants/__tests__/call-recording-field-universal-identifiers.test.ts rename to packages/twenty-apps/public/call-recorder/src/constants/__tests__/call-recording-field-universal-identifiers.test.ts diff --git a/packages/twenty-apps/internal/call-recorder/src/constants/app-description.ts b/packages/twenty-apps/public/call-recorder/src/constants/app-description.ts similarity index 100% rename from packages/twenty-apps/internal/call-recorder/src/constants/app-description.ts rename to packages/twenty-apps/public/call-recorder/src/constants/app-description.ts diff --git a/packages/twenty-apps/internal/call-recorder/src/constants/app-display-name.ts b/packages/twenty-apps/public/call-recorder/src/constants/app-display-name.ts similarity index 100% rename from packages/twenty-apps/internal/call-recorder/src/constants/app-display-name.ts rename to packages/twenty-apps/public/call-recorder/src/constants/app-display-name.ts diff --git a/packages/twenty-apps/internal/call-recorder/src/constants/application-universal-identifier.ts b/packages/twenty-apps/public/call-recorder/src/constants/application-universal-identifier.ts similarity index 100% rename from packages/twenty-apps/internal/call-recorder/src/constants/application-universal-identifier.ts rename to packages/twenty-apps/public/call-recorder/src/constants/application-universal-identifier.ts diff --git a/packages/twenty-apps/internal/call-recorder/src/constants/calendar-event-reconciliation-logic-function-universal-identifier.ts b/packages/twenty-apps/public/call-recorder/src/constants/calendar-event-reconciliation-logic-function-universal-identifier.ts similarity index 100% rename from packages/twenty-apps/internal/call-recorder/src/constants/calendar-event-reconciliation-logic-function-universal-identifier.ts rename to packages/twenty-apps/public/call-recorder/src/constants/calendar-event-reconciliation-logic-function-universal-identifier.ts diff --git a/packages/twenty-apps/internal/call-recorder/src/constants/calendar-event-record-page-layout-universal-identifier.ts b/packages/twenty-apps/public/call-recorder/src/constants/calendar-event-record-page-layout-universal-identifier.ts similarity index 100% rename from packages/twenty-apps/internal/call-recorder/src/constants/calendar-event-record-page-layout-universal-identifier.ts rename to packages/twenty-apps/public/call-recorder/src/constants/calendar-event-record-page-layout-universal-identifier.ts diff --git a/packages/twenty-apps/internal/call-recorder/src/constants/calendar-event-recording-front-component-universal-identifier.ts b/packages/twenty-apps/public/call-recorder/src/constants/calendar-event-recording-front-component-universal-identifier.ts similarity index 100% rename from packages/twenty-apps/internal/call-recorder/src/constants/calendar-event-recording-front-component-universal-identifier.ts rename to packages/twenty-apps/public/call-recorder/src/constants/calendar-event-recording-front-component-universal-identifier.ts diff --git a/packages/twenty-apps/internal/call-recorder/src/constants/calendar-event-recording-page-layout-tab-universal-identifier.ts b/packages/twenty-apps/public/call-recorder/src/constants/calendar-event-recording-page-layout-tab-universal-identifier.ts similarity index 100% rename from packages/twenty-apps/internal/call-recorder/src/constants/calendar-event-recording-page-layout-tab-universal-identifier.ts rename to packages/twenty-apps/public/call-recorder/src/constants/calendar-event-recording-page-layout-tab-universal-identifier.ts diff --git a/packages/twenty-apps/internal/call-recorder/src/constants/calendar-event-recording-page-layout-widget-universal-identifier.ts b/packages/twenty-apps/public/call-recorder/src/constants/calendar-event-recording-page-layout-widget-universal-identifier.ts similarity index 100% rename from packages/twenty-apps/internal/call-recorder/src/constants/calendar-event-recording-page-layout-widget-universal-identifier.ts rename to packages/twenty-apps/public/call-recorder/src/constants/calendar-event-recording-page-layout-widget-universal-identifier.ts diff --git a/packages/twenty-apps/internal/call-recorder/src/constants/call-recorder-everyone-left-timeout-seconds-app-variable-universal-identifier.ts b/packages/twenty-apps/public/call-recorder/src/constants/call-recorder-everyone-left-timeout-seconds-app-variable-universal-identifier.ts similarity index 100% rename from packages/twenty-apps/internal/call-recorder/src/constants/call-recorder-everyone-left-timeout-seconds-app-variable-universal-identifier.ts rename to packages/twenty-apps/public/call-recorder/src/constants/call-recorder-everyone-left-timeout-seconds-app-variable-universal-identifier.ts diff --git a/packages/twenty-apps/internal/call-recorder/src/constants/call-recorder-failure-reason-on-call-recording-field-universal-identifier.ts b/packages/twenty-apps/public/call-recorder/src/constants/call-recorder-failure-reason-on-call-recording-field-universal-identifier.ts similarity index 100% rename from packages/twenty-apps/internal/call-recorder/src/constants/call-recorder-failure-reason-on-call-recording-field-universal-identifier.ts rename to packages/twenty-apps/public/call-recorder/src/constants/call-recorder-failure-reason-on-call-recording-field-universal-identifier.ts diff --git a/packages/twenty-apps/internal/call-recorder/src/constants/call-recorder-join-early-minutes-app-variable-universal-identifier.ts b/packages/twenty-apps/public/call-recorder/src/constants/call-recorder-join-early-minutes-app-variable-universal-identifier.ts similarity index 100% rename from packages/twenty-apps/internal/call-recorder/src/constants/call-recorder-join-early-minutes-app-variable-universal-identifier.ts rename to packages/twenty-apps/public/call-recorder/src/constants/call-recorder-join-early-minutes-app-variable-universal-identifier.ts diff --git a/packages/twenty-apps/internal/call-recorder/src/constants/call-recorder-name-app-variable-universal-identifier.ts b/packages/twenty-apps/public/call-recorder/src/constants/call-recorder-name-app-variable-universal-identifier.ts similarity index 100% rename from packages/twenty-apps/internal/call-recorder/src/constants/call-recorder-name-app-variable-universal-identifier.ts rename to packages/twenty-apps/public/call-recorder/src/constants/call-recorder-name-app-variable-universal-identifier.ts diff --git a/packages/twenty-apps/internal/call-recorder/src/constants/call-recorder-noone-joined-timeout-seconds-app-variable-universal-identifier.ts b/packages/twenty-apps/public/call-recorder/src/constants/call-recorder-noone-joined-timeout-seconds-app-variable-universal-identifier.ts similarity index 100% rename from packages/twenty-apps/internal/call-recorder/src/constants/call-recorder-noone-joined-timeout-seconds-app-variable-universal-identifier.ts rename to packages/twenty-apps/public/call-recorder/src/constants/call-recorder-noone-joined-timeout-seconds-app-variable-universal-identifier.ts diff --git a/packages/twenty-apps/internal/call-recorder/src/constants/call-recorder-preference-off-option-id.ts b/packages/twenty-apps/public/call-recorder/src/constants/call-recorder-preference-off-option-id.ts similarity index 100% rename from packages/twenty-apps/internal/call-recorder/src/constants/call-recorder-preference-off-option-id.ts rename to packages/twenty-apps/public/call-recorder/src/constants/call-recorder-preference-off-option-id.ts diff --git a/packages/twenty-apps/internal/call-recorder/src/constants/call-recorder-preference-on-calendar-event-field-universal-identifier.ts b/packages/twenty-apps/public/call-recorder/src/constants/call-recorder-preference-on-calendar-event-field-universal-identifier.ts similarity index 100% rename from packages/twenty-apps/internal/call-recorder/src/constants/call-recorder-preference-on-calendar-event-field-universal-identifier.ts rename to packages/twenty-apps/public/call-recorder/src/constants/call-recorder-preference-on-calendar-event-field-universal-identifier.ts diff --git a/packages/twenty-apps/internal/call-recorder/src/constants/call-recorder-preference-on-calendar-event-view-field-universal-identifier.ts b/packages/twenty-apps/public/call-recorder/src/constants/call-recorder-preference-on-calendar-event-view-field-universal-identifier.ts similarity index 100% rename from packages/twenty-apps/internal/call-recorder/src/constants/call-recorder-preference-on-calendar-event-view-field-universal-identifier.ts rename to packages/twenty-apps/public/call-recorder/src/constants/call-recorder-preference-on-calendar-event-view-field-universal-identifier.ts diff --git a/packages/twenty-apps/internal/call-recorder/src/constants/call-recorder-preference-on-option-id.ts b/packages/twenty-apps/public/call-recorder/src/constants/call-recorder-preference-on-option-id.ts similarity index 100% rename from packages/twenty-apps/internal/call-recorder/src/constants/call-recorder-preference-on-option-id.ts rename to packages/twenty-apps/public/call-recorder/src/constants/call-recorder-preference-on-option-id.ts diff --git a/packages/twenty-apps/internal/call-recorder/src/constants/call-recorder-preference.ts b/packages/twenty-apps/public/call-recorder/src/constants/call-recorder-preference.ts similarity index 100% rename from packages/twenty-apps/internal/call-recorder/src/constants/call-recorder-preference.ts rename to packages/twenty-apps/public/call-recorder/src/constants/call-recorder-preference.ts diff --git a/packages/twenty-apps/internal/call-recorder/src/constants/call-recorder-waiting-room-timeout-seconds-app-variable-universal-identifier.ts b/packages/twenty-apps/public/call-recorder/src/constants/call-recorder-waiting-room-timeout-seconds-app-variable-universal-identifier.ts similarity index 100% rename from packages/twenty-apps/internal/call-recorder/src/constants/call-recorder-waiting-room-timeout-seconds-app-variable-universal-identifier.ts rename to packages/twenty-apps/public/call-recorder/src/constants/call-recorder-waiting-room-timeout-seconds-app-variable-universal-identifier.ts diff --git a/packages/twenty-apps/internal/call-recorder/src/constants/call-recording-audio-field-universal-identifier.ts b/packages/twenty-apps/public/call-recorder/src/constants/call-recording-audio-field-universal-identifier.ts similarity index 100% rename from packages/twenty-apps/internal/call-recorder/src/constants/call-recording-audio-field-universal-identifier.ts rename to packages/twenty-apps/public/call-recorder/src/constants/call-recording-audio-field-universal-identifier.ts diff --git a/packages/twenty-apps/internal/call-recorder/src/constants/call-recording-video-field-universal-identifier.ts b/packages/twenty-apps/public/call-recorder/src/constants/call-recording-video-field-universal-identifier.ts similarity index 100% rename from packages/twenty-apps/internal/call-recorder/src/constants/call-recording-video-field-universal-identifier.ts rename to packages/twenty-apps/public/call-recorder/src/constants/call-recording-video-field-universal-identifier.ts diff --git a/packages/twenty-apps/internal/call-recorder/src/constants/default-role-universal-identifier.ts b/packages/twenty-apps/public/call-recorder/src/constants/default-role-universal-identifier.ts similarity index 100% rename from packages/twenty-apps/internal/call-recorder/src/constants/default-role-universal-identifier.ts rename to packages/twenty-apps/public/call-recorder/src/constants/default-role-universal-identifier.ts diff --git a/packages/twenty-apps/internal/call-recorder/src/constants/recall-webhook-logic-function-universal-identifier.ts b/packages/twenty-apps/public/call-recorder/src/constants/recall-webhook-logic-function-universal-identifier.ts similarity index 100% rename from packages/twenty-apps/internal/call-recorder/src/constants/recall-webhook-logic-function-universal-identifier.ts rename to packages/twenty-apps/public/call-recorder/src/constants/recall-webhook-logic-function-universal-identifier.ts diff --git a/packages/twenty-apps/internal/call-recorder/src/constants/stale-bot-state-logic-function-universal-identifier.ts b/packages/twenty-apps/public/call-recorder/src/constants/stale-bot-state-logic-function-universal-identifier.ts similarity index 100% rename from packages/twenty-apps/internal/call-recorder/src/constants/stale-bot-state-logic-function-universal-identifier.ts rename to packages/twenty-apps/public/call-recorder/src/constants/stale-bot-state-logic-function-universal-identifier.ts diff --git a/packages/twenty-apps/internal/call-recorder/src/default-role.ts b/packages/twenty-apps/public/call-recorder/src/default-role.ts similarity index 100% rename from packages/twenty-apps/internal/call-recorder/src/default-role.ts rename to packages/twenty-apps/public/call-recorder/src/default-role.ts diff --git a/packages/twenty-apps/internal/call-recorder/src/fields/call-recorder-failure-reason-on-call-recording.field.ts b/packages/twenty-apps/public/call-recorder/src/fields/call-recorder-failure-reason-on-call-recording.field.ts similarity index 100% rename from packages/twenty-apps/internal/call-recorder/src/fields/call-recorder-failure-reason-on-call-recording.field.ts rename to packages/twenty-apps/public/call-recorder/src/fields/call-recorder-failure-reason-on-call-recording.field.ts diff --git a/packages/twenty-apps/internal/call-recorder/src/fields/call-recorder-preference-on-calendar-event.field.ts b/packages/twenty-apps/public/call-recorder/src/fields/call-recorder-preference-on-calendar-event.field.ts similarity index 100% rename from packages/twenty-apps/internal/call-recorder/src/fields/call-recorder-preference-on-calendar-event.field.ts rename to packages/twenty-apps/public/call-recorder/src/fields/call-recorder-preference-on-calendar-event.field.ts diff --git a/packages/twenty-apps/internal/call-recorder/src/front-components/calendar-event-recording.front-component.tsx b/packages/twenty-apps/public/call-recorder/src/front-components/calendar-event-recording.front-component.tsx similarity index 100% rename from packages/twenty-apps/internal/call-recorder/src/front-components/calendar-event-recording.front-component.tsx rename to packages/twenty-apps/public/call-recorder/src/front-components/calendar-event-recording.front-component.tsx diff --git a/packages/twenty-apps/internal/call-recorder/src/front-components/components/CalendarEventRecording.tsx b/packages/twenty-apps/public/call-recorder/src/front-components/components/CalendarEventRecording.tsx similarity index 100% rename from packages/twenty-apps/internal/call-recorder/src/front-components/components/CalendarEventRecording.tsx rename to packages/twenty-apps/public/call-recorder/src/front-components/components/CalendarEventRecording.tsx diff --git a/packages/twenty-apps/internal/call-recorder/src/front-components/components/CalendarEventRecordingBody.tsx b/packages/twenty-apps/public/call-recorder/src/front-components/components/CalendarEventRecordingBody.tsx similarity index 100% rename from packages/twenty-apps/internal/call-recorder/src/front-components/components/CalendarEventRecordingBody.tsx rename to packages/twenty-apps/public/call-recorder/src/front-components/components/CalendarEventRecordingBody.tsx diff --git a/packages/twenty-apps/internal/call-recorder/src/front-components/components/CalendarEventRecordingContent.tsx b/packages/twenty-apps/public/call-recorder/src/front-components/components/CalendarEventRecordingContent.tsx similarity index 100% rename from packages/twenty-apps/internal/call-recorder/src/front-components/components/CalendarEventRecordingContent.tsx rename to packages/twenty-apps/public/call-recorder/src/front-components/components/CalendarEventRecordingContent.tsx diff --git a/packages/twenty-apps/internal/call-recorder/src/front-components/components/RecordingTranscript.tsx b/packages/twenty-apps/public/call-recorder/src/front-components/components/RecordingTranscript.tsx similarity index 100% rename from packages/twenty-apps/internal/call-recorder/src/front-components/components/RecordingTranscript.tsx rename to packages/twenty-apps/public/call-recorder/src/front-components/components/RecordingTranscript.tsx diff --git a/packages/twenty-apps/internal/call-recorder/src/front-components/components/RecordingVideoPlayer.tsx b/packages/twenty-apps/public/call-recorder/src/front-components/components/RecordingVideoPlayer.tsx similarity index 100% rename from packages/twenty-apps/internal/call-recorder/src/front-components/components/RecordingVideoPlayer.tsx rename to packages/twenty-apps/public/call-recorder/src/front-components/components/RecordingVideoPlayer.tsx diff --git a/packages/twenty-apps/internal/call-recorder/src/front-components/components/TranscriptEntryList.tsx b/packages/twenty-apps/public/call-recorder/src/front-components/components/TranscriptEntryList.tsx similarity index 100% rename from packages/twenty-apps/internal/call-recorder/src/front-components/components/TranscriptEntryList.tsx rename to packages/twenty-apps/public/call-recorder/src/front-components/components/TranscriptEntryList.tsx diff --git a/packages/twenty-apps/internal/call-recorder/src/front-components/components/TranscriptEntryListItem.tsx b/packages/twenty-apps/public/call-recorder/src/front-components/components/TranscriptEntryListItem.tsx similarity index 100% rename from packages/twenty-apps/internal/call-recorder/src/front-components/components/TranscriptEntryListItem.tsx rename to packages/twenty-apps/public/call-recorder/src/front-components/components/TranscriptEntryListItem.tsx diff --git a/packages/twenty-apps/internal/call-recorder/src/front-components/components/TranscriptErrorBox.tsx b/packages/twenty-apps/public/call-recorder/src/front-components/components/TranscriptErrorBox.tsx similarity index 100% rename from packages/twenty-apps/internal/call-recorder/src/front-components/components/TranscriptErrorBox.tsx rename to packages/twenty-apps/public/call-recorder/src/front-components/components/TranscriptErrorBox.tsx diff --git a/packages/twenty-apps/internal/call-recorder/src/front-components/components/TranscriptSpeakerAvatar.tsx b/packages/twenty-apps/public/call-recorder/src/front-components/components/TranscriptSpeakerAvatar.tsx similarity index 100% rename from packages/twenty-apps/internal/call-recorder/src/front-components/components/TranscriptSpeakerAvatar.tsx rename to packages/twenty-apps/public/call-recorder/src/front-components/components/TranscriptSpeakerAvatar.tsx diff --git a/packages/twenty-apps/internal/call-recorder/src/front-components/components/TranscriptSpeakerChip.tsx b/packages/twenty-apps/public/call-recorder/src/front-components/components/TranscriptSpeakerChip.tsx similarity index 100% rename from packages/twenty-apps/internal/call-recorder/src/front-components/components/TranscriptSpeakerChip.tsx rename to packages/twenty-apps/public/call-recorder/src/front-components/components/TranscriptSpeakerChip.tsx diff --git a/packages/twenty-apps/internal/call-recorder/src/front-components/constants/recording-theme-css-variables.ts b/packages/twenty-apps/public/call-recorder/src/front-components/constants/recording-theme-css-variables.ts similarity index 100% rename from packages/twenty-apps/internal/call-recorder/src/front-components/constants/recording-theme-css-variables.ts rename to packages/twenty-apps/public/call-recorder/src/front-components/constants/recording-theme-css-variables.ts diff --git a/packages/twenty-apps/internal/call-recorder/src/front-components/hooks/use-calendar-event-participants.ts b/packages/twenty-apps/public/call-recorder/src/front-components/hooks/use-calendar-event-participants.ts similarity index 100% rename from packages/twenty-apps/internal/call-recorder/src/front-components/hooks/use-calendar-event-participants.ts rename to packages/twenty-apps/public/call-recorder/src/front-components/hooks/use-calendar-event-participants.ts diff --git a/packages/twenty-apps/internal/call-recorder/src/front-components/hooks/use-calendar-event-recording.ts b/packages/twenty-apps/public/call-recorder/src/front-components/hooks/use-calendar-event-recording.ts similarity index 100% rename from packages/twenty-apps/internal/call-recorder/src/front-components/hooks/use-calendar-event-recording.ts rename to packages/twenty-apps/public/call-recorder/src/front-components/hooks/use-calendar-event-recording.ts diff --git a/packages/twenty-apps/internal/call-recorder/src/front-components/types/calendar-event-participant-by-speaker-name.type.ts b/packages/twenty-apps/public/call-recorder/src/front-components/types/calendar-event-participant-by-speaker-name.type.ts similarity index 100% rename from packages/twenty-apps/internal/call-recorder/src/front-components/types/calendar-event-participant-by-speaker-name.type.ts rename to packages/twenty-apps/public/call-recorder/src/front-components/types/calendar-event-participant-by-speaker-name.type.ts diff --git a/packages/twenty-apps/internal/call-recorder/src/front-components/types/calendar-event-recording-participant.type.ts b/packages/twenty-apps/public/call-recorder/src/front-components/types/calendar-event-recording-participant.type.ts similarity index 100% rename from packages/twenty-apps/internal/call-recorder/src/front-components/types/calendar-event-recording-participant.type.ts rename to packages/twenty-apps/public/call-recorder/src/front-components/types/calendar-event-recording-participant.type.ts diff --git a/packages/twenty-apps/internal/call-recorder/src/front-components/types/transcript-entry.type.ts b/packages/twenty-apps/public/call-recorder/src/front-components/types/transcript-entry.type.ts similarity index 100% rename from packages/twenty-apps/internal/call-recorder/src/front-components/types/transcript-entry.type.ts rename to packages/twenty-apps/public/call-recorder/src/front-components/types/transcript-entry.type.ts diff --git a/packages/twenty-apps/internal/call-recorder/src/front-components/utils/__tests__/find-active-transcript-entry-index.test.ts b/packages/twenty-apps/public/call-recorder/src/front-components/utils/__tests__/find-active-transcript-entry-index.test.ts similarity index 100% rename from packages/twenty-apps/internal/call-recorder/src/front-components/utils/__tests__/find-active-transcript-entry-index.test.ts rename to packages/twenty-apps/public/call-recorder/src/front-components/utils/__tests__/find-active-transcript-entry-index.test.ts diff --git a/packages/twenty-apps/internal/call-recorder/src/front-components/utils/__tests__/format-transcript-timestamp.test.ts b/packages/twenty-apps/public/call-recorder/src/front-components/utils/__tests__/format-transcript-timestamp.test.ts similarity index 100% rename from packages/twenty-apps/internal/call-recorder/src/front-components/utils/__tests__/format-transcript-timestamp.test.ts rename to packages/twenty-apps/public/call-recorder/src/front-components/utils/__tests__/format-transcript-timestamp.test.ts diff --git a/packages/twenty-apps/internal/call-recorder/src/front-components/utils/__tests__/get-speaker-name-match-keys.test.ts b/packages/twenty-apps/public/call-recorder/src/front-components/utils/__tests__/get-speaker-name-match-keys.test.ts similarity index 100% rename from packages/twenty-apps/internal/call-recorder/src/front-components/utils/__tests__/get-speaker-name-match-keys.test.ts rename to packages/twenty-apps/public/call-recorder/src/front-components/utils/__tests__/get-speaker-name-match-keys.test.ts diff --git a/packages/twenty-apps/internal/call-recorder/src/front-components/utils/__tests__/parse-transcript-entries.test.ts b/packages/twenty-apps/public/call-recorder/src/front-components/utils/__tests__/parse-transcript-entries.test.ts similarity index 100% rename from packages/twenty-apps/internal/call-recorder/src/front-components/utils/__tests__/parse-transcript-entries.test.ts rename to packages/twenty-apps/public/call-recorder/src/front-components/utils/__tests__/parse-transcript-entries.test.ts diff --git a/packages/twenty-apps/internal/call-recorder/src/front-components/utils/build-calendar-event-participant-by-speaker-name.util.ts b/packages/twenty-apps/public/call-recorder/src/front-components/utils/build-calendar-event-participant-by-speaker-name.util.ts similarity index 100% rename from packages/twenty-apps/internal/call-recorder/src/front-components/utils/build-calendar-event-participant-by-speaker-name.util.ts rename to packages/twenty-apps/public/call-recorder/src/front-components/utils/build-calendar-event-participant-by-speaker-name.util.ts diff --git a/packages/twenty-apps/internal/call-recorder/src/front-components/utils/find-active-transcript-entry-index.util.ts b/packages/twenty-apps/public/call-recorder/src/front-components/utils/find-active-transcript-entry-index.util.ts similarity index 100% rename from packages/twenty-apps/internal/call-recorder/src/front-components/utils/find-active-transcript-entry-index.util.ts rename to packages/twenty-apps/public/call-recorder/src/front-components/utils/find-active-transcript-entry-index.util.ts diff --git a/packages/twenty-apps/internal/call-recorder/src/front-components/utils/format-transcript-timestamp.util.ts b/packages/twenty-apps/public/call-recorder/src/front-components/utils/format-transcript-timestamp.util.ts similarity index 100% rename from packages/twenty-apps/internal/call-recorder/src/front-components/utils/format-transcript-timestamp.util.ts rename to packages/twenty-apps/public/call-recorder/src/front-components/utils/format-transcript-timestamp.util.ts diff --git a/packages/twenty-apps/internal/call-recorder/src/front-components/utils/get-absolute-avatar-url.util.ts b/packages/twenty-apps/public/call-recorder/src/front-components/utils/get-absolute-avatar-url.util.ts similarity index 100% rename from packages/twenty-apps/internal/call-recorder/src/front-components/utils/get-absolute-avatar-url.util.ts rename to packages/twenty-apps/public/call-recorder/src/front-components/utils/get-absolute-avatar-url.util.ts diff --git a/packages/twenty-apps/internal/call-recorder/src/front-components/utils/get-calendar-event-participant-for-speaker-name.util.ts b/packages/twenty-apps/public/call-recorder/src/front-components/utils/get-calendar-event-participant-for-speaker-name.util.ts similarity index 100% rename from packages/twenty-apps/internal/call-recorder/src/front-components/utils/get-calendar-event-participant-for-speaker-name.util.ts rename to packages/twenty-apps/public/call-recorder/src/front-components/utils/get-calendar-event-participant-for-speaker-name.util.ts diff --git a/packages/twenty-apps/internal/call-recorder/src/front-components/utils/get-speaker-name-match-keys.util.ts b/packages/twenty-apps/public/call-recorder/src/front-components/utils/get-speaker-name-match-keys.util.ts similarity index 100% rename from packages/twenty-apps/internal/call-recorder/src/front-components/utils/get-speaker-name-match-keys.util.ts rename to packages/twenty-apps/public/call-recorder/src/front-components/utils/get-speaker-name-match-keys.util.ts diff --git a/packages/twenty-apps/internal/call-recorder/src/front-components/utils/get-video-file-extension.util.ts b/packages/twenty-apps/public/call-recorder/src/front-components/utils/get-video-file-extension.util.ts similarity index 100% rename from packages/twenty-apps/internal/call-recorder/src/front-components/utils/get-video-file-extension.util.ts rename to packages/twenty-apps/public/call-recorder/src/front-components/utils/get-video-file-extension.util.ts diff --git a/packages/twenty-apps/internal/call-recorder/src/front-components/utils/parse-transcript-entries.util.ts b/packages/twenty-apps/public/call-recorder/src/front-components/utils/parse-transcript-entries.util.ts similarity index 100% rename from packages/twenty-apps/internal/call-recorder/src/front-components/utils/parse-transcript-entries.util.ts rename to packages/twenty-apps/public/call-recorder/src/front-components/utils/parse-transcript-entries.util.ts diff --git a/packages/twenty-apps/internal/call-recorder/src/logic-functions/__tests__/recall-webhook.test.ts b/packages/twenty-apps/public/call-recorder/src/logic-functions/__tests__/recall-webhook.test.ts similarity index 100% rename from packages/twenty-apps/internal/call-recorder/src/logic-functions/__tests__/recall-webhook.test.ts rename to packages/twenty-apps/public/call-recorder/src/logic-functions/__tests__/recall-webhook.test.ts diff --git a/packages/twenty-apps/internal/call-recorder/src/logic-functions/constants/call-recorder-everyone-left-timeout-seconds-env-var-name.ts b/packages/twenty-apps/public/call-recorder/src/logic-functions/constants/call-recorder-everyone-left-timeout-seconds-env-var-name.ts similarity index 100% rename from packages/twenty-apps/internal/call-recorder/src/logic-functions/constants/call-recorder-everyone-left-timeout-seconds-env-var-name.ts rename to packages/twenty-apps/public/call-recorder/src/logic-functions/constants/call-recorder-everyone-left-timeout-seconds-env-var-name.ts diff --git a/packages/twenty-apps/internal/call-recorder/src/logic-functions/constants/call-recorder-everyone-left-timeout-seconds.ts b/packages/twenty-apps/public/call-recorder/src/logic-functions/constants/call-recorder-everyone-left-timeout-seconds.ts similarity index 100% rename from packages/twenty-apps/internal/call-recorder/src/logic-functions/constants/call-recorder-everyone-left-timeout-seconds.ts rename to packages/twenty-apps/public/call-recorder/src/logic-functions/constants/call-recorder-everyone-left-timeout-seconds.ts diff --git a/packages/twenty-apps/internal/call-recorder/src/logic-functions/constants/call-recorder-join-early-minutes-env-var-name.ts b/packages/twenty-apps/public/call-recorder/src/logic-functions/constants/call-recorder-join-early-minutes-env-var-name.ts similarity index 100% rename from packages/twenty-apps/internal/call-recorder/src/logic-functions/constants/call-recorder-join-early-minutes-env-var-name.ts rename to packages/twenty-apps/public/call-recorder/src/logic-functions/constants/call-recorder-join-early-minutes-env-var-name.ts diff --git a/packages/twenty-apps/internal/call-recorder/src/logic-functions/constants/call-recorder-name-env-var-name.ts b/packages/twenty-apps/public/call-recorder/src/logic-functions/constants/call-recorder-name-env-var-name.ts similarity index 100% rename from packages/twenty-apps/internal/call-recorder/src/logic-functions/constants/call-recorder-name-env-var-name.ts rename to packages/twenty-apps/public/call-recorder/src/logic-functions/constants/call-recorder-name-env-var-name.ts diff --git a/packages/twenty-apps/internal/call-recorder/src/logic-functions/constants/call-recorder-noone-joined-timeout-seconds-env-var-name.ts b/packages/twenty-apps/public/call-recorder/src/logic-functions/constants/call-recorder-noone-joined-timeout-seconds-env-var-name.ts similarity index 100% rename from packages/twenty-apps/internal/call-recorder/src/logic-functions/constants/call-recorder-noone-joined-timeout-seconds-env-var-name.ts rename to packages/twenty-apps/public/call-recorder/src/logic-functions/constants/call-recorder-noone-joined-timeout-seconds-env-var-name.ts diff --git a/packages/twenty-apps/internal/call-recorder/src/logic-functions/constants/call-recorder-noone-joined-timeout-seconds.ts b/packages/twenty-apps/public/call-recorder/src/logic-functions/constants/call-recorder-noone-joined-timeout-seconds.ts similarity index 100% rename from packages/twenty-apps/internal/call-recorder/src/logic-functions/constants/call-recorder-noone-joined-timeout-seconds.ts rename to packages/twenty-apps/public/call-recorder/src/logic-functions/constants/call-recorder-noone-joined-timeout-seconds.ts diff --git a/packages/twenty-apps/internal/call-recorder/src/logic-functions/constants/call-recorder-recording-retention-hours-env-var-name.ts b/packages/twenty-apps/public/call-recorder/src/logic-functions/constants/call-recorder-recording-retention-hours-env-var-name.ts similarity index 100% rename from packages/twenty-apps/internal/call-recorder/src/logic-functions/constants/call-recorder-recording-retention-hours-env-var-name.ts rename to packages/twenty-apps/public/call-recorder/src/logic-functions/constants/call-recorder-recording-retention-hours-env-var-name.ts diff --git a/packages/twenty-apps/internal/call-recorder/src/logic-functions/constants/call-recorder-waiting-room-timeout-seconds-env-var-name.ts b/packages/twenty-apps/public/call-recorder/src/logic-functions/constants/call-recorder-waiting-room-timeout-seconds-env-var-name.ts similarity index 100% rename from packages/twenty-apps/internal/call-recorder/src/logic-functions/constants/call-recorder-waiting-room-timeout-seconds-env-var-name.ts rename to packages/twenty-apps/public/call-recorder/src/logic-functions/constants/call-recorder-waiting-room-timeout-seconds-env-var-name.ts diff --git a/packages/twenty-apps/internal/call-recorder/src/logic-functions/constants/call-recorder-waiting-room-timeout-seconds.ts b/packages/twenty-apps/public/call-recorder/src/logic-functions/constants/call-recorder-waiting-room-timeout-seconds.ts similarity index 100% rename from packages/twenty-apps/internal/call-recorder/src/logic-functions/constants/call-recorder-waiting-room-timeout-seconds.ts rename to packages/twenty-apps/public/call-recorder/src/logic-functions/constants/call-recorder-waiting-room-timeout-seconds.ts diff --git a/packages/twenty-apps/internal/call-recorder/src/logic-functions/constants/call-recording-micro-credits-per-hour.ts b/packages/twenty-apps/public/call-recorder/src/logic-functions/constants/call-recording-micro-credits-per-hour.ts similarity index 100% rename from packages/twenty-apps/internal/call-recorder/src/logic-functions/constants/call-recording-micro-credits-per-hour.ts rename to packages/twenty-apps/public/call-recorder/src/logic-functions/constants/call-recording-micro-credits-per-hour.ts diff --git a/packages/twenty-apps/internal/call-recorder/src/logic-functions/constants/call-recording-request-status.ts b/packages/twenty-apps/public/call-recorder/src/logic-functions/constants/call-recording-request-status.ts similarity index 100% rename from packages/twenty-apps/internal/call-recorder/src/logic-functions/constants/call-recording-request-status.ts rename to packages/twenty-apps/public/call-recorder/src/logic-functions/constants/call-recording-request-status.ts diff --git a/packages/twenty-apps/internal/call-recorder/src/logic-functions/constants/call-recording-status.ts b/packages/twenty-apps/public/call-recorder/src/logic-functions/constants/call-recording-status.ts similarity index 100% rename from packages/twenty-apps/internal/call-recorder/src/logic-functions/constants/call-recording-status.ts rename to packages/twenty-apps/public/call-recorder/src/logic-functions/constants/call-recording-status.ts diff --git a/packages/twenty-apps/internal/call-recorder/src/logic-functions/constants/default-call-recorder-join-early-minutes.ts b/packages/twenty-apps/public/call-recorder/src/logic-functions/constants/default-call-recorder-join-early-minutes.ts similarity index 100% rename from packages/twenty-apps/internal/call-recorder/src/logic-functions/constants/default-call-recorder-join-early-minutes.ts rename to packages/twenty-apps/public/call-recorder/src/logic-functions/constants/default-call-recorder-join-early-minutes.ts diff --git a/packages/twenty-apps/internal/call-recorder/src/logic-functions/constants/default-call-recorder-name.ts b/packages/twenty-apps/public/call-recorder/src/logic-functions/constants/default-call-recorder-name.ts similarity index 100% rename from packages/twenty-apps/internal/call-recorder/src/logic-functions/constants/default-call-recorder-name.ts rename to packages/twenty-apps/public/call-recorder/src/logic-functions/constants/default-call-recorder-name.ts diff --git a/packages/twenty-apps/internal/call-recorder/src/logic-functions/constants/default-call-recorder-recording-retention-hours.ts b/packages/twenty-apps/public/call-recorder/src/logic-functions/constants/default-call-recorder-recording-retention-hours.ts similarity index 100% rename from packages/twenty-apps/internal/call-recorder/src/logic-functions/constants/default-call-recorder-recording-retention-hours.ts rename to packages/twenty-apps/public/call-recorder/src/logic-functions/constants/default-call-recorder-recording-retention-hours.ts diff --git a/packages/twenty-apps/internal/call-recorder/src/logic-functions/constants/default-recall-region.ts b/packages/twenty-apps/public/call-recorder/src/logic-functions/constants/default-recall-region.ts similarity index 100% rename from packages/twenty-apps/internal/call-recorder/src/logic-functions/constants/default-recall-region.ts rename to packages/twenty-apps/public/call-recorder/src/logic-functions/constants/default-recall-region.ts diff --git a/packages/twenty-apps/internal/call-recorder/src/logic-functions/constants/milliseconds-per-minute.ts b/packages/twenty-apps/public/call-recorder/src/logic-functions/constants/milliseconds-per-minute.ts similarity index 100% rename from packages/twenty-apps/internal/call-recorder/src/logic-functions/constants/milliseconds-per-minute.ts rename to packages/twenty-apps/public/call-recorder/src/logic-functions/constants/milliseconds-per-minute.ts diff --git a/packages/twenty-apps/internal/call-recorder/src/logic-functions/constants/non-terminal-call-recording-statuses.ts b/packages/twenty-apps/public/call-recorder/src/logic-functions/constants/non-terminal-call-recording-statuses.ts similarity index 100% rename from packages/twenty-apps/internal/call-recorder/src/logic-functions/constants/non-terminal-call-recording-statuses.ts rename to packages/twenty-apps/public/call-recorder/src/logic-functions/constants/non-terminal-call-recording-statuses.ts diff --git a/packages/twenty-apps/internal/call-recorder/src/logic-functions/constants/recall-api-key-env-var-name.ts b/packages/twenty-apps/public/call-recorder/src/logic-functions/constants/recall-api-key-env-var-name.ts similarity index 100% rename from packages/twenty-apps/internal/call-recorder/src/logic-functions/constants/recall-api-key-env-var-name.ts rename to packages/twenty-apps/public/call-recorder/src/logic-functions/constants/recall-api-key-env-var-name.ts diff --git a/packages/twenty-apps/internal/call-recorder/src/logic-functions/constants/recall-api-max-attempts.ts b/packages/twenty-apps/public/call-recorder/src/logic-functions/constants/recall-api-max-attempts.ts similarity index 100% rename from packages/twenty-apps/internal/call-recorder/src/logic-functions/constants/recall-api-max-attempts.ts rename to packages/twenty-apps/public/call-recorder/src/logic-functions/constants/recall-api-max-attempts.ts diff --git a/packages/twenty-apps/internal/call-recorder/src/logic-functions/constants/recall-api-retry-delay-ms.ts b/packages/twenty-apps/public/call-recorder/src/logic-functions/constants/recall-api-retry-delay-ms.ts similarity index 100% rename from packages/twenty-apps/internal/call-recorder/src/logic-functions/constants/recall-api-retry-delay-ms.ts rename to packages/twenty-apps/public/call-recorder/src/logic-functions/constants/recall-api-retry-delay-ms.ts diff --git a/packages/twenty-apps/internal/call-recorder/src/logic-functions/constants/recall-bot-automatic-leave.ts b/packages/twenty-apps/public/call-recorder/src/logic-functions/constants/recall-bot-automatic-leave.ts similarity index 100% rename from packages/twenty-apps/internal/call-recorder/src/logic-functions/constants/recall-bot-automatic-leave.ts rename to packages/twenty-apps/public/call-recorder/src/logic-functions/constants/recall-bot-automatic-leave.ts diff --git a/packages/twenty-apps/internal/call-recorder/src/logic-functions/constants/recall-bot-everyone-left-min-activate-after-seconds.ts b/packages/twenty-apps/public/call-recorder/src/logic-functions/constants/recall-bot-everyone-left-min-activate-after-seconds.ts similarity index 100% rename from packages/twenty-apps/internal/call-recorder/src/logic-functions/constants/recall-bot-everyone-left-min-activate-after-seconds.ts rename to packages/twenty-apps/public/call-recorder/src/logic-functions/constants/recall-bot-everyone-left-min-activate-after-seconds.ts diff --git a/packages/twenty-apps/internal/call-recorder/src/logic-functions/constants/recall-bot-recording-config.ts b/packages/twenty-apps/public/call-recorder/src/logic-functions/constants/recall-bot-recording-config.ts similarity index 100% rename from packages/twenty-apps/internal/call-recorder/src/logic-functions/constants/recall-bot-recording-config.ts rename to packages/twenty-apps/public/call-recorder/src/logic-functions/constants/recall-bot-recording-config.ts diff --git a/packages/twenty-apps/internal/call-recorder/src/logic-functions/constants/recall-region-env-var-name.ts b/packages/twenty-apps/public/call-recorder/src/logic-functions/constants/recall-region-env-var-name.ts similarity index 100% rename from packages/twenty-apps/internal/call-recorder/src/logic-functions/constants/recall-region-env-var-name.ts rename to packages/twenty-apps/public/call-recorder/src/logic-functions/constants/recall-region-env-var-name.ts diff --git a/packages/twenty-apps/internal/call-recorder/src/logic-functions/constants/recall-webhook-secret-env-var-name.ts b/packages/twenty-apps/public/call-recorder/src/logic-functions/constants/recall-webhook-secret-env-var-name.ts similarity index 100% rename from packages/twenty-apps/internal/call-recorder/src/logic-functions/constants/recall-webhook-secret-env-var-name.ts rename to packages/twenty-apps/public/call-recorder/src/logic-functions/constants/recall-webhook-secret-env-var-name.ts diff --git a/packages/twenty-apps/internal/call-recorder/src/logic-functions/constants/restricted-field-placeholder.ts b/packages/twenty-apps/public/call-recorder/src/logic-functions/constants/restricted-field-placeholder.ts similarity index 100% rename from packages/twenty-apps/internal/call-recorder/src/logic-functions/constants/restricted-field-placeholder.ts rename to packages/twenty-apps/public/call-recorder/src/logic-functions/constants/restricted-field-placeholder.ts diff --git a/packages/twenty-apps/internal/call-recorder/src/logic-functions/constants/stale-bot-state-cron-pattern.ts b/packages/twenty-apps/public/call-recorder/src/logic-functions/constants/stale-bot-state-cron-pattern.ts similarity index 100% rename from packages/twenty-apps/internal/call-recorder/src/logic-functions/constants/stale-bot-state-cron-pattern.ts rename to packages/twenty-apps/public/call-recorder/src/logic-functions/constants/stale-bot-state-cron-pattern.ts diff --git a/packages/twenty-apps/internal/call-recorder/src/logic-functions/constants/twenty-page-size.ts b/packages/twenty-apps/public/call-recorder/src/logic-functions/constants/twenty-page-size.ts similarity index 100% rename from packages/twenty-apps/internal/call-recorder/src/logic-functions/constants/twenty-page-size.ts rename to packages/twenty-apps/public/call-recorder/src/logic-functions/constants/twenty-page-size.ts diff --git a/packages/twenty-apps/internal/call-recorder/src/logic-functions/data/__tests__/complete-call-recording-ingestion.test.ts b/packages/twenty-apps/public/call-recorder/src/logic-functions/data/__tests__/complete-call-recording-ingestion.test.ts similarity index 100% rename from packages/twenty-apps/internal/call-recorder/src/logic-functions/data/__tests__/complete-call-recording-ingestion.test.ts rename to packages/twenty-apps/public/call-recorder/src/logic-functions/data/__tests__/complete-call-recording-ingestion.test.ts diff --git a/packages/twenty-apps/internal/call-recorder/src/logic-functions/data/__tests__/fetch-all-nodes.test.ts b/packages/twenty-apps/public/call-recorder/src/logic-functions/data/__tests__/fetch-all-nodes.test.ts similarity index 100% rename from packages/twenty-apps/internal/call-recorder/src/logic-functions/data/__tests__/fetch-all-nodes.test.ts rename to packages/twenty-apps/public/call-recorder/src/logic-functions/data/__tests__/fetch-all-nodes.test.ts diff --git a/packages/twenty-apps/internal/call-recorder/src/logic-functions/data/__tests__/get-current-workspace-id.test.ts b/packages/twenty-apps/public/call-recorder/src/logic-functions/data/__tests__/get-current-workspace-id.test.ts similarity index 100% rename from packages/twenty-apps/internal/call-recorder/src/logic-functions/data/__tests__/get-current-workspace-id.test.ts rename to packages/twenty-apps/public/call-recorder/src/logic-functions/data/__tests__/get-current-workspace-id.test.ts diff --git a/packages/twenty-apps/internal/call-recorder/src/logic-functions/data/__tests__/strip-restricted-field-value.test.ts b/packages/twenty-apps/public/call-recorder/src/logic-functions/data/__tests__/strip-restricted-field-value.test.ts similarity index 100% rename from packages/twenty-apps/internal/call-recorder/src/logic-functions/data/__tests__/strip-restricted-field-value.test.ts rename to packages/twenty-apps/public/call-recorder/src/logic-functions/data/__tests__/strip-restricted-field-value.test.ts diff --git a/packages/twenty-apps/internal/call-recorder/src/logic-functions/data/complete-call-recording-ingestion.util.ts b/packages/twenty-apps/public/call-recorder/src/logic-functions/data/complete-call-recording-ingestion.util.ts similarity index 100% rename from packages/twenty-apps/internal/call-recorder/src/logic-functions/data/complete-call-recording-ingestion.util.ts rename to packages/twenty-apps/public/call-recorder/src/logic-functions/data/complete-call-recording-ingestion.util.ts diff --git a/packages/twenty-apps/internal/call-recorder/src/logic-functions/data/create-call-recording.util.ts b/packages/twenty-apps/public/call-recorder/src/logic-functions/data/create-call-recording.util.ts similarity index 100% rename from packages/twenty-apps/internal/call-recorder/src/logic-functions/data/create-call-recording.util.ts rename to packages/twenty-apps/public/call-recorder/src/logic-functions/data/create-call-recording.util.ts diff --git a/packages/twenty-apps/internal/call-recorder/src/logic-functions/data/execute-current-schema-mutation.util.ts b/packages/twenty-apps/public/call-recorder/src/logic-functions/data/execute-current-schema-mutation.util.ts similarity index 100% rename from packages/twenty-apps/internal/call-recorder/src/logic-functions/data/execute-current-schema-mutation.util.ts rename to packages/twenty-apps/public/call-recorder/src/logic-functions/data/execute-current-schema-mutation.util.ts diff --git a/packages/twenty-apps/internal/call-recorder/src/logic-functions/data/fetch-all-nodes.util.ts b/packages/twenty-apps/public/call-recorder/src/logic-functions/data/fetch-all-nodes.util.ts similarity index 100% rename from packages/twenty-apps/internal/call-recorder/src/logic-functions/data/fetch-all-nodes.util.ts rename to packages/twenty-apps/public/call-recorder/src/logic-functions/data/fetch-all-nodes.util.ts diff --git a/packages/twenty-apps/internal/call-recorder/src/logic-functions/data/fetch-calendar-events-by-filter.util.ts b/packages/twenty-apps/public/call-recorder/src/logic-functions/data/fetch-calendar-events-by-filter.util.ts similarity index 100% rename from packages/twenty-apps/internal/call-recorder/src/logic-functions/data/fetch-calendar-events-by-filter.util.ts rename to packages/twenty-apps/public/call-recorder/src/logic-functions/data/fetch-calendar-events-by-filter.util.ts diff --git a/packages/twenty-apps/internal/call-recorder/src/logic-functions/data/fetch-calendar-events-by-ids.util.ts b/packages/twenty-apps/public/call-recorder/src/logic-functions/data/fetch-calendar-events-by-ids.util.ts similarity index 100% rename from packages/twenty-apps/internal/call-recorder/src/logic-functions/data/fetch-calendar-events-by-ids.util.ts rename to packages/twenty-apps/public/call-recorder/src/logic-functions/data/fetch-calendar-events-by-ids.util.ts diff --git a/packages/twenty-apps/internal/call-recorder/src/logic-functions/data/fetch-calendar-events-by-starts-at-values.util.ts b/packages/twenty-apps/public/call-recorder/src/logic-functions/data/fetch-calendar-events-by-starts-at-values.util.ts similarity index 100% rename from packages/twenty-apps/internal/call-recorder/src/logic-functions/data/fetch-calendar-events-by-starts-at-values.util.ts rename to packages/twenty-apps/public/call-recorder/src/logic-functions/data/fetch-calendar-events-by-starts-at-values.util.ts diff --git a/packages/twenty-apps/internal/call-recorder/src/logic-functions/data/find-call-recordings-by-calendar-event-ids.util.ts b/packages/twenty-apps/public/call-recorder/src/logic-functions/data/find-call-recordings-by-calendar-event-ids.util.ts similarity index 100% rename from packages/twenty-apps/internal/call-recorder/src/logic-functions/data/find-call-recordings-by-calendar-event-ids.util.ts rename to packages/twenty-apps/public/call-recorder/src/logic-functions/data/find-call-recordings-by-calendar-event-ids.util.ts diff --git a/packages/twenty-apps/internal/call-recorder/src/logic-functions/data/find-call-recordings-by-filter.util.ts b/packages/twenty-apps/public/call-recorder/src/logic-functions/data/find-call-recordings-by-filter.util.ts similarity index 100% rename from packages/twenty-apps/internal/call-recorder/src/logic-functions/data/find-call-recordings-by-filter.util.ts rename to packages/twenty-apps/public/call-recorder/src/logic-functions/data/find-call-recordings-by-filter.util.ts diff --git a/packages/twenty-apps/internal/call-recorder/src/logic-functions/data/find-call-recordings-by-ids.util.ts b/packages/twenty-apps/public/call-recorder/src/logic-functions/data/find-call-recordings-by-ids.util.ts similarity index 100% rename from packages/twenty-apps/internal/call-recorder/src/logic-functions/data/find-call-recordings-by-ids.util.ts rename to packages/twenty-apps/public/call-recorder/src/logic-functions/data/find-call-recordings-by-ids.util.ts diff --git a/packages/twenty-apps/internal/call-recorder/src/logic-functions/data/find-open-scheduled-call-recordings.util.ts b/packages/twenty-apps/public/call-recorder/src/logic-functions/data/find-open-scheduled-call-recordings.util.ts similarity index 100% rename from packages/twenty-apps/internal/call-recorder/src/logic-functions/data/find-open-scheduled-call-recordings.util.ts rename to packages/twenty-apps/public/call-recorder/src/logic-functions/data/find-open-scheduled-call-recordings.util.ts diff --git a/packages/twenty-apps/internal/call-recorder/src/logic-functions/data/get-current-workspace-id.util.ts b/packages/twenty-apps/public/call-recorder/src/logic-functions/data/get-current-workspace-id.util.ts similarity index 100% rename from packages/twenty-apps/internal/call-recorder/src/logic-functions/data/get-current-workspace-id.util.ts rename to packages/twenty-apps/public/call-recorder/src/logic-functions/data/get-current-workspace-id.util.ts diff --git a/packages/twenty-apps/internal/call-recorder/src/logic-functions/data/strip-restricted-field-value.util.ts b/packages/twenty-apps/public/call-recorder/src/logic-functions/data/strip-restricted-field-value.util.ts similarity index 100% rename from packages/twenty-apps/internal/call-recorder/src/logic-functions/data/strip-restricted-field-value.util.ts rename to packages/twenty-apps/public/call-recorder/src/logic-functions/data/strip-restricted-field-value.util.ts diff --git a/packages/twenty-apps/internal/call-recorder/src/logic-functions/data/update-call-recording.util.ts b/packages/twenty-apps/public/call-recorder/src/logic-functions/data/update-call-recording.util.ts similarity index 100% rename from packages/twenty-apps/internal/call-recorder/src/logic-functions/data/update-call-recording.util.ts rename to packages/twenty-apps/public/call-recorder/src/logic-functions/data/update-call-recording.util.ts diff --git a/packages/twenty-apps/internal/call-recorder/src/logic-functions/domain/__tests__/build-call-recorder-policy-result.test.ts b/packages/twenty-apps/public/call-recorder/src/logic-functions/domain/__tests__/build-call-recorder-policy-result.test.ts similarity index 100% rename from packages/twenty-apps/internal/call-recorder/src/logic-functions/domain/__tests__/build-call-recorder-policy-result.test.ts rename to packages/twenty-apps/public/call-recorder/src/logic-functions/domain/__tests__/build-call-recorder-policy-result.test.ts diff --git a/packages/twenty-apps/internal/call-recorder/src/logic-functions/domain/__tests__/compute-call-recording-charge.test.ts b/packages/twenty-apps/public/call-recorder/src/logic-functions/domain/__tests__/compute-call-recording-charge.test.ts similarity index 100% rename from packages/twenty-apps/internal/call-recorder/src/logic-functions/domain/__tests__/compute-call-recording-charge.test.ts rename to packages/twenty-apps/public/call-recorder/src/logic-functions/domain/__tests__/compute-call-recording-charge.test.ts diff --git a/packages/twenty-apps/internal/call-recorder/src/logic-functions/domain/__tests__/compute-call-recording-id-for-meeting.test.ts b/packages/twenty-apps/public/call-recorder/src/logic-functions/domain/__tests__/compute-call-recording-id-for-meeting.test.ts similarity index 100% rename from packages/twenty-apps/internal/call-recorder/src/logic-functions/domain/__tests__/compute-call-recording-id-for-meeting.test.ts rename to packages/twenty-apps/public/call-recorder/src/logic-functions/domain/__tests__/compute-call-recording-id-for-meeting.test.ts diff --git a/packages/twenty-apps/internal/call-recorder/src/logic-functions/domain/__tests__/compute-real-meeting-key.test.ts b/packages/twenty-apps/public/call-recorder/src/logic-functions/domain/__tests__/compute-real-meeting-key.test.ts similarity index 100% rename from packages/twenty-apps/internal/call-recorder/src/logic-functions/domain/__tests__/compute-real-meeting-key.test.ts rename to packages/twenty-apps/public/call-recorder/src/logic-functions/domain/__tests__/compute-real-meeting-key.test.ts diff --git a/packages/twenty-apps/internal/call-recorder/src/logic-functions/domain/__tests__/is-call-recording-ingestion-complete.test.ts b/packages/twenty-apps/public/call-recorder/src/logic-functions/domain/__tests__/is-call-recording-ingestion-complete.test.ts similarity index 100% rename from packages/twenty-apps/internal/call-recorder/src/logic-functions/domain/__tests__/is-call-recording-ingestion-complete.test.ts rename to packages/twenty-apps/public/call-recorder/src/logic-functions/domain/__tests__/is-call-recording-ingestion-complete.test.ts diff --git a/packages/twenty-apps/internal/call-recorder/src/logic-functions/domain/__tests__/is-call-recording-status-downgrade.test.ts b/packages/twenty-apps/public/call-recorder/src/logic-functions/domain/__tests__/is-call-recording-status-downgrade.test.ts similarity index 100% rename from packages/twenty-apps/internal/call-recorder/src/logic-functions/domain/__tests__/is-call-recording-status-downgrade.test.ts rename to packages/twenty-apps/public/call-recorder/src/logic-functions/domain/__tests__/is-call-recording-status-downgrade.test.ts diff --git a/packages/twenty-apps/internal/call-recorder/src/logic-functions/domain/__tests__/resolve-call-recorder-policy-result.test.ts b/packages/twenty-apps/public/call-recorder/src/logic-functions/domain/__tests__/resolve-call-recorder-policy-result.test.ts similarity index 100% rename from packages/twenty-apps/internal/call-recorder/src/logic-functions/domain/__tests__/resolve-call-recorder-policy-result.test.ts rename to packages/twenty-apps/public/call-recorder/src/logic-functions/domain/__tests__/resolve-call-recorder-policy-result.test.ts diff --git a/packages/twenty-apps/internal/call-recorder/src/logic-functions/domain/__tests__/should-complete-call-recording-ingestion.test.ts b/packages/twenty-apps/public/call-recorder/src/logic-functions/domain/__tests__/should-complete-call-recording-ingestion.test.ts similarity index 100% rename from packages/twenty-apps/internal/call-recorder/src/logic-functions/domain/__tests__/should-complete-call-recording-ingestion.test.ts rename to packages/twenty-apps/public/call-recorder/src/logic-functions/domain/__tests__/should-complete-call-recording-ingestion.test.ts diff --git a/packages/twenty-apps/internal/call-recorder/src/logic-functions/domain/aggregate-call-recorder-policy-results-by-meeting.util.ts b/packages/twenty-apps/public/call-recorder/src/logic-functions/domain/aggregate-call-recorder-policy-results-by-meeting.util.ts similarity index 100% rename from packages/twenty-apps/internal/call-recorder/src/logic-functions/domain/aggregate-call-recorder-policy-results-by-meeting.util.ts rename to packages/twenty-apps/public/call-recorder/src/logic-functions/domain/aggregate-call-recorder-policy-results-by-meeting.util.ts diff --git a/packages/twenty-apps/internal/call-recorder/src/logic-functions/domain/build-call-recorder-policy-result.util.ts b/packages/twenty-apps/public/call-recorder/src/logic-functions/domain/build-call-recorder-policy-result.util.ts similarity index 100% rename from packages/twenty-apps/internal/call-recorder/src/logic-functions/domain/build-call-recorder-policy-result.util.ts rename to packages/twenty-apps/public/call-recorder/src/logic-functions/domain/build-call-recorder-policy-result.util.ts diff --git a/packages/twenty-apps/internal/call-recorder/src/logic-functions/domain/build-failed-transcript-marker.util.ts b/packages/twenty-apps/public/call-recorder/src/logic-functions/domain/build-failed-transcript-marker.util.ts similarity index 100% rename from packages/twenty-apps/internal/call-recorder/src/logic-functions/domain/build-failed-transcript-marker.util.ts rename to packages/twenty-apps/public/call-recorder/src/logic-functions/domain/build-failed-transcript-marker.util.ts diff --git a/packages/twenty-apps/internal/call-recorder/src/logic-functions/domain/build-pending-transcript-marker.util.ts b/packages/twenty-apps/public/call-recorder/src/logic-functions/domain/build-pending-transcript-marker.util.ts similarity index 100% rename from packages/twenty-apps/internal/call-recorder/src/logic-functions/domain/build-pending-transcript-marker.util.ts rename to packages/twenty-apps/public/call-recorder/src/logic-functions/domain/build-pending-transcript-marker.util.ts diff --git a/packages/twenty-apps/internal/call-recorder/src/logic-functions/domain/build-recall-bot-metadata.util.ts b/packages/twenty-apps/public/call-recorder/src/logic-functions/domain/build-recall-bot-metadata.util.ts similarity index 100% rename from packages/twenty-apps/internal/call-recorder/src/logic-functions/domain/build-recall-bot-metadata.util.ts rename to packages/twenty-apps/public/call-recorder/src/logic-functions/domain/build-recall-bot-metadata.util.ts diff --git a/packages/twenty-apps/internal/call-recorder/src/logic-functions/domain/build-transcript-failure-reason.util.ts b/packages/twenty-apps/public/call-recorder/src/logic-functions/domain/build-transcript-failure-reason.util.ts similarity index 100% rename from packages/twenty-apps/internal/call-recorder/src/logic-functions/domain/build-transcript-failure-reason.util.ts rename to packages/twenty-apps/public/call-recorder/src/logic-functions/domain/build-transcript-failure-reason.util.ts diff --git a/packages/twenty-apps/internal/call-recorder/src/logic-functions/domain/compute-call-recording-charge.util.ts b/packages/twenty-apps/public/call-recorder/src/logic-functions/domain/compute-call-recording-charge.util.ts similarity index 100% rename from packages/twenty-apps/internal/call-recorder/src/logic-functions/domain/compute-call-recording-charge.util.ts rename to packages/twenty-apps/public/call-recorder/src/logic-functions/domain/compute-call-recording-charge.util.ts diff --git a/packages/twenty-apps/internal/call-recorder/src/logic-functions/domain/compute-call-recording-id-for-meeting.util.ts b/packages/twenty-apps/public/call-recorder/src/logic-functions/domain/compute-call-recording-id-for-meeting.util.ts similarity index 100% rename from packages/twenty-apps/internal/call-recorder/src/logic-functions/domain/compute-call-recording-id-for-meeting.util.ts rename to packages/twenty-apps/public/call-recorder/src/logic-functions/domain/compute-call-recording-id-for-meeting.util.ts diff --git a/packages/twenty-apps/internal/call-recorder/src/logic-functions/domain/compute-real-meeting-key.util.ts b/packages/twenty-apps/public/call-recorder/src/logic-functions/domain/compute-real-meeting-key.util.ts similarity index 100% rename from packages/twenty-apps/internal/call-recorder/src/logic-functions/domain/compute-real-meeting-key.util.ts rename to packages/twenty-apps/public/call-recorder/src/logic-functions/domain/compute-real-meeting-key.util.ts diff --git a/packages/twenty-apps/internal/call-recorder/src/logic-functions/domain/compute-recall-bot-join-at.util.ts b/packages/twenty-apps/public/call-recorder/src/logic-functions/domain/compute-recall-bot-join-at.util.ts similarity index 100% rename from packages/twenty-apps/internal/call-recorder/src/logic-functions/domain/compute-recall-bot-join-at.util.ts rename to packages/twenty-apps/public/call-recorder/src/logic-functions/domain/compute-recall-bot-join-at.util.ts diff --git a/packages/twenty-apps/internal/call-recorder/src/logic-functions/domain/is-call-recording-ingestion-complete.util.ts b/packages/twenty-apps/public/call-recorder/src/logic-functions/domain/is-call-recording-ingestion-complete.util.ts similarity index 100% rename from packages/twenty-apps/internal/call-recorder/src/logic-functions/domain/is-call-recording-ingestion-complete.util.ts rename to packages/twenty-apps/public/call-recorder/src/logic-functions/domain/is-call-recording-ingestion-complete.util.ts diff --git a/packages/twenty-apps/internal/call-recorder/src/logic-functions/domain/is-call-recording-status-downgrade.util.ts b/packages/twenty-apps/public/call-recorder/src/logic-functions/domain/is-call-recording-status-downgrade.util.ts similarity index 100% rename from packages/twenty-apps/internal/call-recorder/src/logic-functions/domain/is-call-recording-status-downgrade.util.ts rename to packages/twenty-apps/public/call-recorder/src/logic-functions/domain/is-call-recording-status-downgrade.util.ts diff --git a/packages/twenty-apps/internal/call-recorder/src/logic-functions/domain/is-recall-recording-done-signal.util.ts b/packages/twenty-apps/public/call-recorder/src/logic-functions/domain/is-recall-recording-done-signal.util.ts similarity index 100% rename from packages/twenty-apps/internal/call-recorder/src/logic-functions/domain/is-recall-recording-done-signal.util.ts rename to packages/twenty-apps/public/call-recorder/src/logic-functions/domain/is-recall-recording-done-signal.util.ts diff --git a/packages/twenty-apps/internal/call-recorder/src/logic-functions/domain/map-recall-status-code-to-call-recording-status.util.ts b/packages/twenty-apps/public/call-recorder/src/logic-functions/domain/map-recall-status-code-to-call-recording-status.util.ts similarity index 100% rename from packages/twenty-apps/internal/call-recorder/src/logic-functions/domain/map-recall-status-code-to-call-recording-status.util.ts rename to packages/twenty-apps/public/call-recorder/src/logic-functions/domain/map-recall-status-code-to-call-recording-status.util.ts diff --git a/packages/twenty-apps/internal/call-recorder/src/logic-functions/domain/parse-transcript-marker.util.ts b/packages/twenty-apps/public/call-recorder/src/logic-functions/domain/parse-transcript-marker.util.ts similarity index 100% rename from packages/twenty-apps/internal/call-recorder/src/logic-functions/domain/parse-transcript-marker.util.ts rename to packages/twenty-apps/public/call-recorder/src/logic-functions/domain/parse-transcript-marker.util.ts diff --git a/packages/twenty-apps/internal/call-recorder/src/logic-functions/domain/resolve-call-recorder-policy-result.util.ts b/packages/twenty-apps/public/call-recorder/src/logic-functions/domain/resolve-call-recorder-policy-result.util.ts similarity index 100% rename from packages/twenty-apps/internal/call-recorder/src/logic-functions/domain/resolve-call-recorder-policy-result.util.ts rename to packages/twenty-apps/public/call-recorder/src/logic-functions/domain/resolve-call-recorder-policy-result.util.ts diff --git a/packages/twenty-apps/internal/call-recorder/src/logic-functions/domain/should-complete-call-recording-ingestion.util.ts b/packages/twenty-apps/public/call-recorder/src/logic-functions/domain/should-complete-call-recording-ingestion.util.ts similarity index 100% rename from packages/twenty-apps/internal/call-recorder/src/logic-functions/domain/should-complete-call-recording-ingestion.util.ts rename to packages/twenty-apps/public/call-recorder/src/logic-functions/domain/should-complete-call-recording-ingestion.util.ts diff --git a/packages/twenty-apps/internal/call-recorder/src/logic-functions/flows/__tests__/charge-completed-call-recording.test.ts b/packages/twenty-apps/public/call-recorder/src/logic-functions/flows/__tests__/charge-completed-call-recording.test.ts similarity index 100% rename from packages/twenty-apps/internal/call-recorder/src/logic-functions/flows/__tests__/charge-completed-call-recording.test.ts rename to packages/twenty-apps/public/call-recorder/src/logic-functions/flows/__tests__/charge-completed-call-recording.test.ts diff --git a/packages/twenty-apps/internal/call-recorder/src/logic-functions/flows/__tests__/complete-and-charge-call-recording.test.ts b/packages/twenty-apps/public/call-recorder/src/logic-functions/flows/__tests__/complete-and-charge-call-recording.test.ts similarity index 100% rename from packages/twenty-apps/internal/call-recorder/src/logic-functions/flows/__tests__/complete-and-charge-call-recording.test.ts rename to packages/twenty-apps/public/call-recorder/src/logic-functions/flows/__tests__/complete-and-charge-call-recording.test.ts diff --git a/packages/twenty-apps/internal/call-recorder/src/logic-functions/flows/__tests__/converge-diverged-call-recordings.test.ts b/packages/twenty-apps/public/call-recorder/src/logic-functions/flows/__tests__/converge-diverged-call-recordings.test.ts similarity index 100% rename from packages/twenty-apps/internal/call-recorder/src/logic-functions/flows/__tests__/converge-diverged-call-recordings.test.ts rename to packages/twenty-apps/public/call-recorder/src/logic-functions/flows/__tests__/converge-diverged-call-recordings.test.ts diff --git a/packages/twenty-apps/internal/call-recorder/src/logic-functions/flows/__tests__/download-transcript.test.ts b/packages/twenty-apps/public/call-recorder/src/logic-functions/flows/__tests__/download-transcript.test.ts similarity index 100% rename from packages/twenty-apps/internal/call-recorder/src/logic-functions/flows/__tests__/download-transcript.test.ts rename to packages/twenty-apps/public/call-recorder/src/logic-functions/flows/__tests__/download-transcript.test.ts diff --git a/packages/twenty-apps/internal/call-recorder/src/logic-functions/flows/__tests__/handle-recall-webhook.test.ts b/packages/twenty-apps/public/call-recorder/src/logic-functions/flows/__tests__/handle-recall-webhook.test.ts similarity index 100% rename from packages/twenty-apps/internal/call-recorder/src/logic-functions/flows/__tests__/handle-recall-webhook.test.ts rename to packages/twenty-apps/public/call-recorder/src/logic-functions/flows/__tests__/handle-recall-webhook.test.ts diff --git a/packages/twenty-apps/internal/call-recorder/src/logic-functions/flows/__tests__/heal-call-recordings-missing-bot.test.ts b/packages/twenty-apps/public/call-recorder/src/logic-functions/flows/__tests__/heal-call-recordings-missing-bot.test.ts similarity index 100% rename from packages/twenty-apps/internal/call-recorder/src/logic-functions/flows/__tests__/heal-call-recordings-missing-bot.test.ts rename to packages/twenty-apps/public/call-recorder/src/logic-functions/flows/__tests__/heal-call-recordings-missing-bot.test.ts diff --git a/packages/twenty-apps/internal/call-recorder/src/logic-functions/flows/__tests__/ingest-call-recording-media.test.ts b/packages/twenty-apps/public/call-recorder/src/logic-functions/flows/__tests__/ingest-call-recording-media.test.ts similarity index 100% rename from packages/twenty-apps/internal/call-recorder/src/logic-functions/flows/__tests__/ingest-call-recording-media.test.ts rename to packages/twenty-apps/public/call-recorder/src/logic-functions/flows/__tests__/ingest-call-recording-media.test.ts diff --git a/packages/twenty-apps/internal/call-recorder/src/logic-functions/flows/__tests__/reap-orphaned-call-recorders.test.ts b/packages/twenty-apps/public/call-recorder/src/logic-functions/flows/__tests__/reap-orphaned-call-recorders.test.ts similarity index 100% rename from packages/twenty-apps/internal/call-recorder/src/logic-functions/flows/__tests__/reap-orphaned-call-recorders.test.ts rename to packages/twenty-apps/public/call-recorder/src/logic-functions/flows/__tests__/reap-orphaned-call-recorders.test.ts diff --git a/packages/twenty-apps/internal/call-recorder/src/logic-functions/flows/__tests__/reconcile-call-recorder.test.ts b/packages/twenty-apps/public/call-recorder/src/logic-functions/flows/__tests__/reconcile-call-recorder.test.ts similarity index 100% rename from packages/twenty-apps/internal/call-recorder/src/logic-functions/flows/__tests__/reconcile-call-recorder.test.ts rename to packages/twenty-apps/public/call-recorder/src/logic-functions/flows/__tests__/reconcile-call-recorder.test.ts diff --git a/packages/twenty-apps/internal/call-recorder/src/logic-functions/flows/cancel-call-recording-request.util.ts b/packages/twenty-apps/public/call-recorder/src/logic-functions/flows/cancel-call-recording-request.util.ts similarity index 100% rename from packages/twenty-apps/internal/call-recorder/src/logic-functions/flows/cancel-call-recording-request.util.ts rename to packages/twenty-apps/public/call-recorder/src/logic-functions/flows/cancel-call-recording-request.util.ts diff --git a/packages/twenty-apps/internal/call-recorder/src/logic-functions/flows/charge-completed-call-recording.util.ts b/packages/twenty-apps/public/call-recorder/src/logic-functions/flows/charge-completed-call-recording.util.ts similarity index 100% rename from packages/twenty-apps/internal/call-recorder/src/logic-functions/flows/charge-completed-call-recording.util.ts rename to packages/twenty-apps/public/call-recorder/src/logic-functions/flows/charge-completed-call-recording.util.ts diff --git a/packages/twenty-apps/internal/call-recorder/src/logic-functions/flows/complete-and-charge-call-recording.util.ts b/packages/twenty-apps/public/call-recorder/src/logic-functions/flows/complete-and-charge-call-recording.util.ts similarity index 100% rename from packages/twenty-apps/internal/call-recorder/src/logic-functions/flows/complete-and-charge-call-recording.util.ts rename to packages/twenty-apps/public/call-recorder/src/logic-functions/flows/complete-and-charge-call-recording.util.ts diff --git a/packages/twenty-apps/internal/call-recorder/src/logic-functions/flows/converge-diverged-call-recordings-result.type.ts b/packages/twenty-apps/public/call-recorder/src/logic-functions/flows/converge-diverged-call-recordings-result.type.ts similarity index 100% rename from packages/twenty-apps/internal/call-recorder/src/logic-functions/flows/converge-diverged-call-recordings-result.type.ts rename to packages/twenty-apps/public/call-recorder/src/logic-functions/flows/converge-diverged-call-recordings-result.type.ts diff --git a/packages/twenty-apps/internal/call-recorder/src/logic-functions/flows/converge-diverged-call-recordings.util.ts b/packages/twenty-apps/public/call-recorder/src/logic-functions/flows/converge-diverged-call-recordings.util.ts similarity index 100% rename from packages/twenty-apps/internal/call-recorder/src/logic-functions/flows/converge-diverged-call-recordings.util.ts rename to packages/twenty-apps/public/call-recorder/src/logic-functions/flows/converge-diverged-call-recordings.util.ts diff --git a/packages/twenty-apps/internal/call-recorder/src/logic-functions/flows/download-transcript.util.ts b/packages/twenty-apps/public/call-recorder/src/logic-functions/flows/download-transcript.util.ts similarity index 100% rename from packages/twenty-apps/internal/call-recorder/src/logic-functions/flows/download-transcript.util.ts rename to packages/twenty-apps/public/call-recorder/src/logic-functions/flows/download-transcript.util.ts diff --git a/packages/twenty-apps/internal/call-recorder/src/logic-functions/flows/ensure-call-recorder.util.ts b/packages/twenty-apps/public/call-recorder/src/logic-functions/flows/ensure-call-recorder.util.ts similarity index 100% rename from packages/twenty-apps/internal/call-recorder/src/logic-functions/flows/ensure-call-recorder.util.ts rename to packages/twenty-apps/public/call-recorder/src/logic-functions/flows/ensure-call-recorder.util.ts diff --git a/packages/twenty-apps/internal/call-recorder/src/logic-functions/flows/handle-recall-webhook.util.ts b/packages/twenty-apps/public/call-recorder/src/logic-functions/flows/handle-recall-webhook.util.ts similarity index 100% rename from packages/twenty-apps/internal/call-recorder/src/logic-functions/flows/handle-recall-webhook.util.ts rename to packages/twenty-apps/public/call-recorder/src/logic-functions/flows/handle-recall-webhook.util.ts diff --git a/packages/twenty-apps/internal/call-recorder/src/logic-functions/flows/heal-call-recordings-missing-bot.util.ts b/packages/twenty-apps/public/call-recorder/src/logic-functions/flows/heal-call-recordings-missing-bot.util.ts similarity index 100% rename from packages/twenty-apps/internal/call-recorder/src/logic-functions/flows/heal-call-recordings-missing-bot.util.ts rename to packages/twenty-apps/public/call-recorder/src/logic-functions/flows/heal-call-recordings-missing-bot.util.ts diff --git a/packages/twenty-apps/internal/call-recorder/src/logic-functions/flows/ingest-call-recording-media.util.ts b/packages/twenty-apps/public/call-recorder/src/logic-functions/flows/ingest-call-recording-media.util.ts similarity index 100% rename from packages/twenty-apps/internal/call-recorder/src/logic-functions/flows/ingest-call-recording-media.util.ts rename to packages/twenty-apps/public/call-recorder/src/logic-functions/flows/ingest-call-recording-media.util.ts diff --git a/packages/twenty-apps/internal/call-recorder/src/logic-functions/flows/persist-call-recording-progress.util.ts b/packages/twenty-apps/public/call-recorder/src/logic-functions/flows/persist-call-recording-progress.util.ts similarity index 100% rename from packages/twenty-apps/internal/call-recorder/src/logic-functions/flows/persist-call-recording-progress.util.ts rename to packages/twenty-apps/public/call-recorder/src/logic-functions/flows/persist-call-recording-progress.util.ts diff --git a/packages/twenty-apps/internal/call-recorder/src/logic-functions/flows/reap-orphaned-call-recorders.util.ts b/packages/twenty-apps/public/call-recorder/src/logic-functions/flows/reap-orphaned-call-recorders.util.ts similarity index 100% rename from packages/twenty-apps/internal/call-recorder/src/logic-functions/flows/reap-orphaned-call-recorders.util.ts rename to packages/twenty-apps/public/call-recorder/src/logic-functions/flows/reap-orphaned-call-recorders.util.ts diff --git a/packages/twenty-apps/internal/call-recorder/src/logic-functions/flows/reconcile-call-recorder.util.ts b/packages/twenty-apps/public/call-recorder/src/logic-functions/flows/reconcile-call-recorder.util.ts similarity index 100% rename from packages/twenty-apps/internal/call-recorder/src/logic-functions/flows/reconcile-call-recorder.util.ts rename to packages/twenty-apps/public/call-recorder/src/logic-functions/flows/reconcile-call-recorder.util.ts diff --git a/packages/twenty-apps/internal/call-recorder/src/logic-functions/flows/reconcile-call-recording-transcript-artifact-result.type.ts b/packages/twenty-apps/public/call-recorder/src/logic-functions/flows/reconcile-call-recording-transcript-artifact-result.type.ts similarity index 100% rename from packages/twenty-apps/internal/call-recorder/src/logic-functions/flows/reconcile-call-recording-transcript-artifact-result.type.ts rename to packages/twenty-apps/public/call-recorder/src/logic-functions/flows/reconcile-call-recording-transcript-artifact-result.type.ts diff --git a/packages/twenty-apps/internal/call-recorder/src/logic-functions/flows/reconcile-call-recording-transcript-artifact.util.ts b/packages/twenty-apps/public/call-recorder/src/logic-functions/flows/reconcile-call-recording-transcript-artifact.util.ts similarity index 100% rename from packages/twenty-apps/internal/call-recorder/src/logic-functions/flows/reconcile-call-recording-transcript-artifact.util.ts rename to packages/twenty-apps/public/call-recorder/src/logic-functions/flows/reconcile-call-recording-transcript-artifact.util.ts diff --git a/packages/twenty-apps/internal/call-recorder/src/logic-functions/flows/reschedule-call-recording-bot.util.ts b/packages/twenty-apps/public/call-recorder/src/logic-functions/flows/reschedule-call-recording-bot.util.ts similarity index 100% rename from packages/twenty-apps/internal/call-recorder/src/logic-functions/flows/reschedule-call-recording-bot.util.ts rename to packages/twenty-apps/public/call-recorder/src/logic-functions/flows/reschedule-call-recording-bot.util.ts diff --git a/packages/twenty-apps/internal/call-recorder/src/logic-functions/recall-api/__tests__/extract-recall-bot-convergence.test.ts b/packages/twenty-apps/public/call-recorder/src/logic-functions/recall-api/__tests__/extract-recall-bot-convergence.test.ts similarity index 100% rename from packages/twenty-apps/internal/call-recorder/src/logic-functions/recall-api/__tests__/extract-recall-bot-convergence.test.ts rename to packages/twenty-apps/public/call-recorder/src/logic-functions/recall-api/__tests__/extract-recall-bot-convergence.test.ts diff --git a/packages/twenty-apps/internal/call-recorder/src/logic-functions/recall-api/__tests__/extract-recall-media-urls.test.ts b/packages/twenty-apps/public/call-recorder/src/logic-functions/recall-api/__tests__/extract-recall-media-urls.test.ts similarity index 100% rename from packages/twenty-apps/internal/call-recorder/src/logic-functions/recall-api/__tests__/extract-recall-media-urls.test.ts rename to packages/twenty-apps/public/call-recorder/src/logic-functions/recall-api/__tests__/extract-recall-media-urls.test.ts diff --git a/packages/twenty-apps/internal/call-recorder/src/logic-functions/recall-api/__tests__/recall-bot-api.test.ts b/packages/twenty-apps/public/call-recorder/src/logic-functions/recall-api/__tests__/recall-bot-api.test.ts similarity index 100% rename from packages/twenty-apps/internal/call-recorder/src/logic-functions/recall-api/__tests__/recall-bot-api.test.ts rename to packages/twenty-apps/public/call-recorder/src/logic-functions/recall-api/__tests__/recall-bot-api.test.ts diff --git a/packages/twenty-apps/internal/call-recorder/src/logic-functions/recall-api/__tests__/verify-recall-webhook-signature.test.ts b/packages/twenty-apps/public/call-recorder/src/logic-functions/recall-api/__tests__/verify-recall-webhook-signature.test.ts similarity index 100% rename from packages/twenty-apps/internal/call-recorder/src/logic-functions/recall-api/__tests__/verify-recall-webhook-signature.test.ts rename to packages/twenty-apps/public/call-recorder/src/logic-functions/recall-api/__tests__/verify-recall-webhook-signature.test.ts diff --git a/packages/twenty-apps/internal/call-recorder/src/logic-functions/recall-api/cancel-recall-bot.util.ts b/packages/twenty-apps/public/call-recorder/src/logic-functions/recall-api/cancel-recall-bot.util.ts similarity index 100% rename from packages/twenty-apps/internal/call-recorder/src/logic-functions/recall-api/cancel-recall-bot.util.ts rename to packages/twenty-apps/public/call-recorder/src/logic-functions/recall-api/cancel-recall-bot.util.ts diff --git a/packages/twenty-apps/internal/call-recorder/src/logic-functions/recall-api/create-async-recall-transcript.util.ts b/packages/twenty-apps/public/call-recorder/src/logic-functions/recall-api/create-async-recall-transcript.util.ts similarity index 100% rename from packages/twenty-apps/internal/call-recorder/src/logic-functions/recall-api/create-async-recall-transcript.util.ts rename to packages/twenty-apps/public/call-recorder/src/logic-functions/recall-api/create-async-recall-transcript.util.ts diff --git a/packages/twenty-apps/internal/call-recorder/src/logic-functions/recall-api/eject-recall-bot.util.ts b/packages/twenty-apps/public/call-recorder/src/logic-functions/recall-api/eject-recall-bot.util.ts similarity index 100% rename from packages/twenty-apps/internal/call-recorder/src/logic-functions/recall-api/eject-recall-bot.util.ts rename to packages/twenty-apps/public/call-recorder/src/logic-functions/recall-api/eject-recall-bot.util.ts diff --git a/packages/twenty-apps/internal/call-recorder/src/logic-functions/recall-api/extract-recall-bot-convergence.util.ts b/packages/twenty-apps/public/call-recorder/src/logic-functions/recall-api/extract-recall-bot-convergence.util.ts similarity index 100% rename from packages/twenty-apps/internal/call-recorder/src/logic-functions/recall-api/extract-recall-bot-convergence.util.ts rename to packages/twenty-apps/public/call-recorder/src/logic-functions/recall-api/extract-recall-bot-convergence.util.ts diff --git a/packages/twenty-apps/internal/call-recorder/src/logic-functions/recall-api/extract-recall-bot-id.util.ts b/packages/twenty-apps/public/call-recorder/src/logic-functions/recall-api/extract-recall-bot-id.util.ts similarity index 100% rename from packages/twenty-apps/internal/call-recorder/src/logic-functions/recall-api/extract-recall-bot-id.util.ts rename to packages/twenty-apps/public/call-recorder/src/logic-functions/recall-api/extract-recall-bot-id.util.ts diff --git a/packages/twenty-apps/internal/call-recorder/src/logic-functions/recall-api/extract-recall-media-urls.util.ts b/packages/twenty-apps/public/call-recorder/src/logic-functions/recall-api/extract-recall-media-urls.util.ts similarity index 100% rename from packages/twenty-apps/internal/call-recorder/src/logic-functions/recall-api/extract-recall-media-urls.util.ts rename to packages/twenty-apps/public/call-recorder/src/logic-functions/recall-api/extract-recall-media-urls.util.ts diff --git a/packages/twenty-apps/internal/call-recorder/src/logic-functions/recall-api/get-recall-api-config.util.ts b/packages/twenty-apps/public/call-recorder/src/logic-functions/recall-api/get-recall-api-config.util.ts similarity index 100% rename from packages/twenty-apps/internal/call-recorder/src/logic-functions/recall-api/get-recall-api-config.util.ts rename to packages/twenty-apps/public/call-recorder/src/logic-functions/recall-api/get-recall-api-config.util.ts diff --git a/packages/twenty-apps/internal/call-recorder/src/logic-functions/recall-api/get-recall-bot.util.ts b/packages/twenty-apps/public/call-recorder/src/logic-functions/recall-api/get-recall-bot.util.ts similarity index 100% rename from packages/twenty-apps/internal/call-recorder/src/logic-functions/recall-api/get-recall-bot.util.ts rename to packages/twenty-apps/public/call-recorder/src/logic-functions/recall-api/get-recall-bot.util.ts diff --git a/packages/twenty-apps/internal/call-recorder/src/logic-functions/recall-api/get-recall-recording.util.ts b/packages/twenty-apps/public/call-recorder/src/logic-functions/recall-api/get-recall-recording.util.ts similarity index 100% rename from packages/twenty-apps/internal/call-recorder/src/logic-functions/recall-api/get-recall-recording.util.ts rename to packages/twenty-apps/public/call-recorder/src/logic-functions/recall-api/get-recall-recording.util.ts diff --git a/packages/twenty-apps/internal/call-recorder/src/logic-functions/recall-api/list-recall-transcripts.util.ts b/packages/twenty-apps/public/call-recorder/src/logic-functions/recall-api/list-recall-transcripts.util.ts similarity index 100% rename from packages/twenty-apps/internal/call-recorder/src/logic-functions/recall-api/list-recall-transcripts.util.ts rename to packages/twenty-apps/public/call-recorder/src/logic-functions/recall-api/list-recall-transcripts.util.ts diff --git a/packages/twenty-apps/internal/call-recorder/src/logic-functions/recall-api/list-scheduled-recall-bots.util.ts b/packages/twenty-apps/public/call-recorder/src/logic-functions/recall-api/list-scheduled-recall-bots.util.ts similarity index 100% rename from packages/twenty-apps/internal/call-recorder/src/logic-functions/recall-api/list-scheduled-recall-bots.util.ts rename to packages/twenty-apps/public/call-recorder/src/logic-functions/recall-api/list-scheduled-recall-bots.util.ts diff --git a/packages/twenty-apps/internal/call-recorder/src/logic-functions/recall-api/normalize-recall-timestamp.util.ts b/packages/twenty-apps/public/call-recorder/src/logic-functions/recall-api/normalize-recall-timestamp.util.ts similarity index 100% rename from packages/twenty-apps/internal/call-recorder/src/logic-functions/recall-api/normalize-recall-timestamp.util.ts rename to packages/twenty-apps/public/call-recorder/src/logic-functions/recall-api/normalize-recall-timestamp.util.ts diff --git a/packages/twenty-apps/internal/call-recorder/src/logic-functions/recall-api/parse-recall-webhook-event.util.ts b/packages/twenty-apps/public/call-recorder/src/logic-functions/recall-api/parse-recall-webhook-event.util.ts similarity index 100% rename from packages/twenty-apps/internal/call-recorder/src/logic-functions/recall-api/parse-recall-webhook-event.util.ts rename to packages/twenty-apps/public/call-recorder/src/logic-functions/recall-api/parse-recall-webhook-event.util.ts diff --git a/packages/twenty-apps/internal/call-recorder/src/logic-functions/recall-api/recall-bot-api-request.util.ts b/packages/twenty-apps/public/call-recorder/src/logic-functions/recall-api/recall-bot-api-request.util.ts similarity index 100% rename from packages/twenty-apps/internal/call-recorder/src/logic-functions/recall-api/recall-bot-api-request.util.ts rename to packages/twenty-apps/public/call-recorder/src/logic-functions/recall-api/recall-bot-api-request.util.ts diff --git a/packages/twenty-apps/internal/call-recorder/src/logic-functions/recall-api/recall-transcript-summary.type.ts b/packages/twenty-apps/public/call-recorder/src/logic-functions/recall-api/recall-transcript-summary.type.ts similarity index 100% rename from packages/twenty-apps/internal/call-recorder/src/logic-functions/recall-api/recall-transcript-summary.type.ts rename to packages/twenty-apps/public/call-recorder/src/logic-functions/recall-api/recall-transcript-summary.type.ts diff --git a/packages/twenty-apps/internal/call-recorder/src/logic-functions/recall-api/reschedule-recall-bot.util.ts b/packages/twenty-apps/public/call-recorder/src/logic-functions/recall-api/reschedule-recall-bot.util.ts similarity index 100% rename from packages/twenty-apps/internal/call-recorder/src/logic-functions/recall-api/reschedule-recall-bot.util.ts rename to packages/twenty-apps/public/call-recorder/src/logic-functions/recall-api/reschedule-recall-bot.util.ts diff --git a/packages/twenty-apps/internal/call-recorder/src/logic-functions/recall-api/retrieve-recall-transcript.util.ts b/packages/twenty-apps/public/call-recorder/src/logic-functions/recall-api/retrieve-recall-transcript.util.ts similarity index 100% rename from packages/twenty-apps/internal/call-recorder/src/logic-functions/recall-api/retrieve-recall-transcript.util.ts rename to packages/twenty-apps/public/call-recorder/src/logic-functions/recall-api/retrieve-recall-transcript.util.ts diff --git a/packages/twenty-apps/internal/call-recorder/src/logic-functions/recall-api/schedule-recall-bot.util.ts b/packages/twenty-apps/public/call-recorder/src/logic-functions/recall-api/schedule-recall-bot.util.ts similarity index 100% rename from packages/twenty-apps/internal/call-recorder/src/logic-functions/recall-api/schedule-recall-bot.util.ts rename to packages/twenty-apps/public/call-recorder/src/logic-functions/recall-api/schedule-recall-bot.util.ts diff --git a/packages/twenty-apps/internal/call-recorder/src/logic-functions/recall-api/verify-recall-webhook-signature.util.ts b/packages/twenty-apps/public/call-recorder/src/logic-functions/recall-api/verify-recall-webhook-signature.util.ts similarity index 100% rename from packages/twenty-apps/internal/call-recorder/src/logic-functions/recall-api/verify-recall-webhook-signature.util.ts rename to packages/twenty-apps/public/call-recorder/src/logic-functions/recall-api/verify-recall-webhook-signature.util.ts diff --git a/packages/twenty-apps/internal/call-recorder/src/logic-functions/recall-webhook.ts b/packages/twenty-apps/public/call-recorder/src/logic-functions/recall-webhook.ts similarity index 100% rename from packages/twenty-apps/internal/call-recorder/src/logic-functions/recall-webhook.ts rename to packages/twenty-apps/public/call-recorder/src/logic-functions/recall-webhook.ts diff --git a/packages/twenty-apps/internal/call-recorder/src/logic-functions/reconcile-call-recorder-calendar-event.ts b/packages/twenty-apps/public/call-recorder/src/logic-functions/reconcile-call-recorder-calendar-event.ts similarity index 100% rename from packages/twenty-apps/internal/call-recorder/src/logic-functions/reconcile-call-recorder-calendar-event.ts rename to packages/twenty-apps/public/call-recorder/src/logic-functions/reconcile-call-recorder-calendar-event.ts diff --git a/packages/twenty-apps/internal/call-recorder/src/logic-functions/reconcile-stale-bot-state.ts b/packages/twenty-apps/public/call-recorder/src/logic-functions/reconcile-stale-bot-state.ts similarity index 100% rename from packages/twenty-apps/internal/call-recorder/src/logic-functions/reconcile-stale-bot-state.ts rename to packages/twenty-apps/public/call-recorder/src/logic-functions/reconcile-stale-bot-state.ts diff --git a/packages/twenty-apps/internal/call-recorder/src/logic-functions/types/calendar-event-record.type.ts b/packages/twenty-apps/public/call-recorder/src/logic-functions/types/calendar-event-record.type.ts similarity index 100% rename from packages/twenty-apps/internal/call-recorder/src/logic-functions/types/calendar-event-record.type.ts rename to packages/twenty-apps/public/call-recorder/src/logic-functions/types/calendar-event-record.type.ts diff --git a/packages/twenty-apps/internal/call-recorder/src/logic-functions/types/call-recorder-policy-calendar-event-input.type.ts b/packages/twenty-apps/public/call-recorder/src/logic-functions/types/call-recorder-policy-calendar-event-input.type.ts similarity index 100% rename from packages/twenty-apps/internal/call-recorder/src/logic-functions/types/call-recorder-policy-calendar-event-input.type.ts rename to packages/twenty-apps/public/call-recorder/src/logic-functions/types/call-recorder-policy-calendar-event-input.type.ts diff --git a/packages/twenty-apps/internal/call-recorder/src/logic-functions/types/call-recorder-policy-input.type.ts b/packages/twenty-apps/public/call-recorder/src/logic-functions/types/call-recorder-policy-input.type.ts similarity index 100% rename from packages/twenty-apps/internal/call-recorder/src/logic-functions/types/call-recorder-policy-input.type.ts rename to packages/twenty-apps/public/call-recorder/src/logic-functions/types/call-recorder-policy-input.type.ts diff --git a/packages/twenty-apps/internal/call-recorder/src/logic-functions/types/call-recorder-policy-not-required-reason.type.ts b/packages/twenty-apps/public/call-recorder/src/logic-functions/types/call-recorder-policy-not-required-reason.type.ts similarity index 100% rename from packages/twenty-apps/internal/call-recorder/src/logic-functions/types/call-recorder-policy-not-required-reason.type.ts rename to packages/twenty-apps/public/call-recorder/src/logic-functions/types/call-recorder-policy-not-required-reason.type.ts diff --git a/packages/twenty-apps/internal/call-recorder/src/logic-functions/types/call-recorder-policy-required-reason.type.ts b/packages/twenty-apps/public/call-recorder/src/logic-functions/types/call-recorder-policy-required-reason.type.ts similarity index 100% rename from packages/twenty-apps/internal/call-recorder/src/logic-functions/types/call-recorder-policy-required-reason.type.ts rename to packages/twenty-apps/public/call-recorder/src/logic-functions/types/call-recorder-policy-required-reason.type.ts diff --git a/packages/twenty-apps/internal/call-recorder/src/logic-functions/types/call-recorder-policy-result-for-calendar-event.type.ts b/packages/twenty-apps/public/call-recorder/src/logic-functions/types/call-recorder-policy-result-for-calendar-event.type.ts similarity index 100% rename from packages/twenty-apps/internal/call-recorder/src/logic-functions/types/call-recorder-policy-result-for-calendar-event.type.ts rename to packages/twenty-apps/public/call-recorder/src/logic-functions/types/call-recorder-policy-result-for-calendar-event.type.ts diff --git a/packages/twenty-apps/internal/call-recorder/src/logic-functions/types/call-recorder-policy-result-for-meeting.type.ts b/packages/twenty-apps/public/call-recorder/src/logic-functions/types/call-recorder-policy-result-for-meeting.type.ts similarity index 100% rename from packages/twenty-apps/internal/call-recorder/src/logic-functions/types/call-recorder-policy-result-for-meeting.type.ts rename to packages/twenty-apps/public/call-recorder/src/logic-functions/types/call-recorder-policy-result-for-meeting.type.ts diff --git a/packages/twenty-apps/internal/call-recorder/src/logic-functions/types/call-recorder-policy-result.type.ts b/packages/twenty-apps/public/call-recorder/src/logic-functions/types/call-recorder-policy-result.type.ts similarity index 100% rename from packages/twenty-apps/internal/call-recorder/src/logic-functions/types/call-recorder-policy-result.type.ts rename to packages/twenty-apps/public/call-recorder/src/logic-functions/types/call-recorder-policy-result.type.ts diff --git a/packages/twenty-apps/internal/call-recorder/src/logic-functions/types/call-recorder-reconciliation-result.type.ts b/packages/twenty-apps/public/call-recorder/src/logic-functions/types/call-recorder-reconciliation-result.type.ts similarity index 100% rename from packages/twenty-apps/internal/call-recorder/src/logic-functions/types/call-recorder-reconciliation-result.type.ts rename to packages/twenty-apps/public/call-recorder/src/logic-functions/types/call-recorder-reconciliation-result.type.ts diff --git a/packages/twenty-apps/internal/call-recorder/src/logic-functions/types/call-recording-record.type.ts b/packages/twenty-apps/public/call-recorder/src/logic-functions/types/call-recording-record.type.ts similarity index 100% rename from packages/twenty-apps/internal/call-recorder/src/logic-functions/types/call-recording-record.type.ts rename to packages/twenty-apps/public/call-recorder/src/logic-functions/types/call-recording-record.type.ts diff --git a/packages/twenty-apps/internal/call-recorder/src/logic-functions/types/files-field-value.type.ts b/packages/twenty-apps/public/call-recorder/src/logic-functions/types/files-field-value.type.ts similarity index 100% rename from packages/twenty-apps/internal/call-recorder/src/logic-functions/types/files-field-value.type.ts rename to packages/twenty-apps/public/call-recorder/src/logic-functions/types/files-field-value.type.ts diff --git a/packages/twenty-apps/internal/call-recorder/src/logic-functions/types/meeting-recording.type.ts b/packages/twenty-apps/public/call-recorder/src/logic-functions/types/meeting-recording.type.ts similarity index 100% rename from packages/twenty-apps/internal/call-recorder/src/logic-functions/types/meeting-recording.type.ts rename to packages/twenty-apps/public/call-recorder/src/logic-functions/types/meeting-recording.type.ts diff --git a/packages/twenty-apps/internal/call-recorder/src/logic-functions/types/recall-bot-metadata.type.ts b/packages/twenty-apps/public/call-recorder/src/logic-functions/types/recall-bot-metadata.type.ts similarity index 100% rename from packages/twenty-apps/internal/call-recorder/src/logic-functions/types/recall-bot-metadata.type.ts rename to packages/twenty-apps/public/call-recorder/src/logic-functions/types/recall-bot-metadata.type.ts diff --git a/packages/twenty-apps/internal/call-recorder/src/logic-functions/types/recall-bot-operation-result.type.ts b/packages/twenty-apps/public/call-recorder/src/logic-functions/types/recall-bot-operation-result.type.ts similarity index 100% rename from packages/twenty-apps/internal/call-recorder/src/logic-functions/types/recall-bot-operation-result.type.ts rename to packages/twenty-apps/public/call-recorder/src/logic-functions/types/recall-bot-operation-result.type.ts diff --git a/packages/twenty-apps/internal/call-recorder/src/logic-functions/types/removed-call-recorder-occurrence.type.ts b/packages/twenty-apps/public/call-recorder/src/logic-functions/types/removed-call-recorder-occurrence.type.ts similarity index 100% rename from packages/twenty-apps/internal/call-recorder/src/logic-functions/types/removed-call-recorder-occurrence.type.ts rename to packages/twenty-apps/public/call-recorder/src/logic-functions/types/removed-call-recorder-occurrence.type.ts diff --git a/packages/twenty-apps/internal/call-recorder/src/logic-functions/types/transcript-marker.type.ts b/packages/twenty-apps/public/call-recorder/src/logic-functions/types/transcript-marker.type.ts similarity index 100% rename from packages/twenty-apps/internal/call-recorder/src/logic-functions/types/transcript-marker.type.ts rename to packages/twenty-apps/public/call-recorder/src/logic-functions/types/transcript-marker.type.ts diff --git a/packages/twenty-apps/internal/call-recorder/src/logic-functions/utils/as-record.util.ts b/packages/twenty-apps/public/call-recorder/src/logic-functions/utils/as-record.util.ts similarity index 100% rename from packages/twenty-apps/internal/call-recorder/src/logic-functions/utils/as-record.util.ts rename to packages/twenty-apps/public/call-recorder/src/logic-functions/utils/as-record.util.ts diff --git a/packages/twenty-apps/internal/call-recorder/src/logic-functions/utils/get-application-variable-value.util.ts b/packages/twenty-apps/public/call-recorder/src/logic-functions/utils/get-application-variable-value.util.ts similarity index 100% rename from packages/twenty-apps/internal/call-recorder/src/logic-functions/utils/get-application-variable-value.util.ts rename to packages/twenty-apps/public/call-recorder/src/logic-functions/utils/get-application-variable-value.util.ts diff --git a/packages/twenty-apps/internal/call-recorder/src/logic-functions/utils/get-record-at-path.util.ts b/packages/twenty-apps/public/call-recorder/src/logic-functions/utils/get-record-at-path.util.ts similarity index 100% rename from packages/twenty-apps/internal/call-recorder/src/logic-functions/utils/get-record-at-path.util.ts rename to packages/twenty-apps/public/call-recorder/src/logic-functions/utils/get-record-at-path.util.ts diff --git a/packages/twenty-apps/internal/call-recorder/src/logic-functions/utils/get-string.util.ts b/packages/twenty-apps/public/call-recorder/src/logic-functions/utils/get-string.util.ts similarity index 100% rename from packages/twenty-apps/internal/call-recorder/src/logic-functions/utils/get-string.util.ts rename to packages/twenty-apps/public/call-recorder/src/logic-functions/utils/get-string.util.ts diff --git a/packages/twenty-apps/internal/call-recorder/src/logic-functions/utils/get-unique-sorted-ids.util.ts b/packages/twenty-apps/public/call-recorder/src/logic-functions/utils/get-unique-sorted-ids.util.ts similarity index 100% rename from packages/twenty-apps/internal/call-recorder/src/logic-functions/utils/get-unique-sorted-ids.util.ts rename to packages/twenty-apps/public/call-recorder/src/logic-functions/utils/get-unique-sorted-ids.util.ts diff --git a/packages/twenty-apps/internal/call-recorder/src/logic-functions/utils/is-non-empty-string.util.ts b/packages/twenty-apps/public/call-recorder/src/logic-functions/utils/is-non-empty-string.util.ts similarity index 100% rename from packages/twenty-apps/internal/call-recorder/src/logic-functions/utils/is-non-empty-string.util.ts rename to packages/twenty-apps/public/call-recorder/src/logic-functions/utils/is-non-empty-string.util.ts diff --git a/packages/twenty-apps/internal/call-recorder/src/page-layouts/calendar-event-recording-tab.ts b/packages/twenty-apps/public/call-recorder/src/page-layouts/calendar-event-recording-tab.ts similarity index 100% rename from packages/twenty-apps/internal/call-recorder/src/page-layouts/calendar-event-recording-tab.ts rename to packages/twenty-apps/public/call-recorder/src/page-layouts/calendar-event-recording-tab.ts diff --git a/packages/twenty-apps/internal/call-recorder/src/view-fields/call-recorder-preference-on-calendar-event.view-field.ts b/packages/twenty-apps/public/call-recorder/src/view-fields/call-recorder-preference-on-calendar-event.view-field.ts similarity index 100% rename from packages/twenty-apps/internal/call-recorder/src/view-fields/call-recorder-preference-on-calendar-event.view-field.ts rename to packages/twenty-apps/public/call-recorder/src/view-fields/call-recorder-preference-on-calendar-event.view-field.ts diff --git a/packages/twenty-apps/internal/call-recorder/tsconfig.json b/packages/twenty-apps/public/call-recorder/tsconfig.json similarity index 100% rename from packages/twenty-apps/internal/call-recorder/tsconfig.json rename to packages/twenty-apps/public/call-recorder/tsconfig.json diff --git a/packages/twenty-apps/internal/call-recorder/tsconfig.spec.json b/packages/twenty-apps/public/call-recorder/tsconfig.spec.json similarity index 100% rename from packages/twenty-apps/internal/call-recorder/tsconfig.spec.json rename to packages/twenty-apps/public/call-recorder/tsconfig.spec.json diff --git a/packages/twenty-apps/internal/call-recorder/vitest.config.ts b/packages/twenty-apps/public/call-recorder/vitest.config.ts similarity index 100% rename from packages/twenty-apps/internal/call-recorder/vitest.config.ts rename to packages/twenty-apps/public/call-recorder/vitest.config.ts diff --git a/packages/twenty-apps/internal/call-recorder/vitest.unit.config.ts b/packages/twenty-apps/public/call-recorder/vitest.unit.config.ts similarity index 100% rename from packages/twenty-apps/internal/call-recorder/vitest.unit.config.ts rename to packages/twenty-apps/public/call-recorder/vitest.unit.config.ts diff --git a/packages/twenty-apps/internal/call-recorder/yarn.lock b/packages/twenty-apps/public/call-recorder/yarn.lock similarity index 100% rename from packages/twenty-apps/internal/call-recorder/yarn.lock rename to packages/twenty-apps/public/call-recorder/yarn.lock diff --git a/packages/twenty-apps/public/twenty-meeting-bot/.env.example b/packages/twenty-apps/public/twenty-meeting-bot/.env.example deleted file mode 100644 index 1a1972fe6047e..0000000000000 --- a/packages/twenty-apps/public/twenty-meeting-bot/.env.example +++ /dev/null @@ -1,5 +0,0 @@ -# Credentials for integration tests. Copy this file to .env.local and fill in the key. -# Get an API key from the Twenty UI: Settings -> APIs & Webhooks. -# .env.local is gitignored; never commit a real key. -TWENTY_API_URL=http://localhost:2020 -TWENTY_API_KEY= diff --git a/packages/twenty-apps/public/twenty-meeting-bot/.gitignore b/packages/twenty-apps/public/twenty-meeting-bot/.gitignore deleted file mode 100644 index 6699cb048ae81..0000000000000 --- a/packages/twenty-apps/public/twenty-meeting-bot/.gitignore +++ /dev/null @@ -1,39 +0,0 @@ -# See https://help.github.com/articles/ignoring-files/ for more about ignoring files. - -# dependencies -/node_modules -/.pnp -.pnp.* -.yarn - -# codegen -generated - -# testing -/coverage - -# dev -/dist/ - -.twenty - -# production -/build - -# misc -.DS_Store -*.pem - -# debug -npm-debug.log* -yarn-debug.log* -yarn-error.log* -.pnpm-debug.log* - -# env files (can opt-in for committing if needed) -.env* -!.env.example - -# typescript -*.tsbuildinfo -*.d.ts diff --git a/packages/twenty-apps/public/twenty-meeting-bot/.nvmrc b/packages/twenty-apps/public/twenty-meeting-bot/.nvmrc deleted file mode 100644 index 341cb50613a01..0000000000000 --- a/packages/twenty-apps/public/twenty-meeting-bot/.nvmrc +++ /dev/null @@ -1 +0,0 @@ -24.5.0 diff --git a/packages/twenty-apps/public/twenty-meeting-bot/.oxlintrc.json b/packages/twenty-apps/public/twenty-meeting-bot/.oxlintrc.json deleted file mode 100644 index 34c54bff59f2a..0000000000000 --- a/packages/twenty-apps/public/twenty-meeting-bot/.oxlintrc.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "$schema": "./node_modules/oxlint/configuration_schema.json", - "extends": ["../../.oxlintrc.base.json"], - "plugins": ["typescript"], - "categories": { - "correctness": "off" - }, - "ignorePatterns": ["node_modules", "dist"], - "rules": { - "no-unused-vars": "off", - - "typescript/no-unused-vars": [ - "warn", - { - "argsIgnorePattern": "^_" - } - ], - "typescript/no-explicit-any": "off" - } -} diff --git a/packages/twenty-apps/public/twenty-meeting-bot/.yarnrc.yml b/packages/twenty-apps/public/twenty-meeting-bot/.yarnrc.yml deleted file mode 100644 index 3186f3f0795ab..0000000000000 --- a/packages/twenty-apps/public/twenty-meeting-bot/.yarnrc.yml +++ /dev/null @@ -1 +0,0 @@ -nodeLinker: node-modules diff --git a/packages/twenty-apps/public/twenty-meeting-bot/AGENTS.md b/packages/twenty-apps/public/twenty-meeting-bot/AGENTS.md deleted file mode 100644 index 9078a7be23425..0000000000000 --- a/packages/twenty-apps/public/twenty-meeting-bot/AGENTS.md +++ /dev/null @@ -1,67 +0,0 @@ -## Base documentation - -- Getting started: - - https://docs.twenty.com/developers/extend/apps/getting-started/quick-start.md - - https://docs.twenty.com/developers/extend/apps/getting-started/concepts.md - - https://docs.twenty.com/developers/extend/apps/getting-started/project-structure.md - - https://docs.twenty.com/developers/extend/apps/getting-started/local-server.md - - https://docs.twenty.com/developers/extend/apps/getting-started/scaffolding.md - - https://docs.twenty.com/developers/extend/apps/getting-started/troubleshooting.md -- Config: - - https://docs.twenty.com/developers/extend/apps/config/overview.md - - https://docs.twenty.com/developers/extend/apps/config/application.md - - https://docs.twenty.com/developers/extend/apps/config/roles.md - - https://docs.twenty.com/developers/extend/apps/config/install-hooks.md - - https://docs.twenty.com/developers/extend/apps/config/public-assets.md -- Data: - - https://docs.twenty.com/developers/extend/apps/data/overview.md - - https://docs.twenty.com/developers/extend/apps/data/objects.md - - https://docs.twenty.com/developers/extend/apps/data/extending-objects.md - - https://docs.twenty.com/developers/extend/apps/data/relations.md -- Logic: - - https://docs.twenty.com/developers/extend/apps/logic/overview.md - - https://docs.twenty.com/developers/extend/apps/logic/logic-functions.md - - https://docs.twenty.com/developers/extend/apps/logic/skills-and-agents.md - - https://docs.twenty.com/developers/extend/apps/logic/connections.md -- Layout: - - https://docs.twenty.com/developers/extend/apps/layout/overview.md - - https://docs.twenty.com/developers/extend/apps/layout/views.md - - https://docs.twenty.com/developers/extend/apps/layout/navigation-menu-items.md - - https://docs.twenty.com/developers/extend/apps/layout/page-layouts.md - - https://docs.twenty.com/developers/extend/apps/layout/front-components.md - - https://docs.twenty.com/developers/extend/apps/layout/command-menu-items.md -- Operations: - - https://docs.twenty.com/developers/extend/apps/operations/overview.md - - https://docs.twenty.com/developers/extend/apps/operations/cli.md - - https://docs.twenty.com/developers/extend/apps/operations/testing.md - - https://docs.twenty.com/developers/extend/apps/operations/publishing.md -- Rich app example: https://github.com/twentyhq/twenty/tree/main/packages/twenty-apps/examples/postcard - -## UUID requirement - -- All generated UUIDs must be valid UUID v4. - -## Common Pitfalls - -- Creating an object without an index view associated. Unless this is a technical object, user will need to visualize it. -- Creating a view without a navigationMenuItem associated. This will make the view unavailable on the left sidebar. -- Creating a front-end component that has a scroll instead of being responsive to its fixed widget height and width, unless it is specifically meant to be used in a canvas tab. - -## Best practice - -It's highly recommended to create new app entities using `yarn twenty dev:add`. These are the options: - -| Entity type | Command | Generated file | -| -------------------- | ---------------------------------------- | ------------------------------------- | -| Object | `yarn twenty dev:add object` | `src/objects/.ts` | -| Field | `yarn twenty dev:add field` | `src/fields/.ts` | -| Logic function | `yarn twenty dev:add logicFunction` | `src/logic-functions/.ts` | -| Front component | `yarn twenty dev:add frontComponent` | `src/front-components/.tsx` | -| Role | `yarn twenty dev:add role` | `src/roles/.ts` | -| Skill | `yarn twenty dev:add skill` | `src/skills/.ts` | -| Agent | `yarn twenty dev:add agent` | `src/agents/.ts` | -| View | `yarn twenty dev:add view` | `src/views/.ts` | -| Navigation menu item | `yarn twenty dev:add navigationMenuItem` | `src/navigation-menu-items/.ts` | -| Page layout | `yarn twenty dev:add pageLayout` | `src/page-layouts/.ts` | - -This helps automatically generate required IDs etc. diff --git a/packages/twenty-apps/public/twenty-meeting-bot/CLAUDE.md b/packages/twenty-apps/public/twenty-meeting-bot/CLAUDE.md deleted file mode 100644 index 9078a7be23425..0000000000000 --- a/packages/twenty-apps/public/twenty-meeting-bot/CLAUDE.md +++ /dev/null @@ -1,67 +0,0 @@ -## Base documentation - -- Getting started: - - https://docs.twenty.com/developers/extend/apps/getting-started/quick-start.md - - https://docs.twenty.com/developers/extend/apps/getting-started/concepts.md - - https://docs.twenty.com/developers/extend/apps/getting-started/project-structure.md - - https://docs.twenty.com/developers/extend/apps/getting-started/local-server.md - - https://docs.twenty.com/developers/extend/apps/getting-started/scaffolding.md - - https://docs.twenty.com/developers/extend/apps/getting-started/troubleshooting.md -- Config: - - https://docs.twenty.com/developers/extend/apps/config/overview.md - - https://docs.twenty.com/developers/extend/apps/config/application.md - - https://docs.twenty.com/developers/extend/apps/config/roles.md - - https://docs.twenty.com/developers/extend/apps/config/install-hooks.md - - https://docs.twenty.com/developers/extend/apps/config/public-assets.md -- Data: - - https://docs.twenty.com/developers/extend/apps/data/overview.md - - https://docs.twenty.com/developers/extend/apps/data/objects.md - - https://docs.twenty.com/developers/extend/apps/data/extending-objects.md - - https://docs.twenty.com/developers/extend/apps/data/relations.md -- Logic: - - https://docs.twenty.com/developers/extend/apps/logic/overview.md - - https://docs.twenty.com/developers/extend/apps/logic/logic-functions.md - - https://docs.twenty.com/developers/extend/apps/logic/skills-and-agents.md - - https://docs.twenty.com/developers/extend/apps/logic/connections.md -- Layout: - - https://docs.twenty.com/developers/extend/apps/layout/overview.md - - https://docs.twenty.com/developers/extend/apps/layout/views.md - - https://docs.twenty.com/developers/extend/apps/layout/navigation-menu-items.md - - https://docs.twenty.com/developers/extend/apps/layout/page-layouts.md - - https://docs.twenty.com/developers/extend/apps/layout/front-components.md - - https://docs.twenty.com/developers/extend/apps/layout/command-menu-items.md -- Operations: - - https://docs.twenty.com/developers/extend/apps/operations/overview.md - - https://docs.twenty.com/developers/extend/apps/operations/cli.md - - https://docs.twenty.com/developers/extend/apps/operations/testing.md - - https://docs.twenty.com/developers/extend/apps/operations/publishing.md -- Rich app example: https://github.com/twentyhq/twenty/tree/main/packages/twenty-apps/examples/postcard - -## UUID requirement - -- All generated UUIDs must be valid UUID v4. - -## Common Pitfalls - -- Creating an object without an index view associated. Unless this is a technical object, user will need to visualize it. -- Creating a view without a navigationMenuItem associated. This will make the view unavailable on the left sidebar. -- Creating a front-end component that has a scroll instead of being responsive to its fixed widget height and width, unless it is specifically meant to be used in a canvas tab. - -## Best practice - -It's highly recommended to create new app entities using `yarn twenty dev:add`. These are the options: - -| Entity type | Command | Generated file | -| -------------------- | ---------------------------------------- | ------------------------------------- | -| Object | `yarn twenty dev:add object` | `src/objects/.ts` | -| Field | `yarn twenty dev:add field` | `src/fields/.ts` | -| Logic function | `yarn twenty dev:add logicFunction` | `src/logic-functions/.ts` | -| Front component | `yarn twenty dev:add frontComponent` | `src/front-components/.tsx` | -| Role | `yarn twenty dev:add role` | `src/roles/.ts` | -| Skill | `yarn twenty dev:add skill` | `src/skills/.ts` | -| Agent | `yarn twenty dev:add agent` | `src/agents/.ts` | -| View | `yarn twenty dev:add view` | `src/views/.ts` | -| Navigation menu item | `yarn twenty dev:add navigationMenuItem` | `src/navigation-menu-items/.ts` | -| Page layout | `yarn twenty dev:add pageLayout` | `src/page-layouts/.ts` | - -This helps automatically generate required IDs etc. diff --git a/packages/twenty-apps/public/twenty-meeting-bot/README.md b/packages/twenty-apps/public/twenty-meeting-bot/README.md deleted file mode 100644 index 1d7ab672ebfb0..0000000000000 --- a/packages/twenty-apps/public/twenty-meeting-bot/README.md +++ /dev/null @@ -1,173 +0,0 @@ -# Twenty Meeting Bot - -Record your meetings automatically and keep every call inside your CRM. Twenty -Meeting Bot sends a recording bot to your team's calendar meetings, then stores -the video, audio, and a speaker-attributed transcript on the meeting's record — -searchable, in context, and ready for people, AI agents, and workflows to act -on. - -## What this app does - -1. A teammate has an upcoming meeting on a synced calendar with a video - conference link (Zoom, Google Meet, Microsoft Teams — anything Recall.ai - supports). -2. Because recording is on by default, the app schedules a meeting bot to join - that event shortly before it starts. -3. The bot joins under the configured display name and records the meeting's - audio and video for its duration. -4. When the call ends, Recall.ai processes the recording; the app ingests the - video, the audio, and a speaker-attributed transcript and stores them as a - **Call Recording**. -5. The recording and transcript surface on the meeting's **Calendar Event**, - under a **Call Recording** tab — ready to review, and for AI agents and - workflows to act on. - -## What gets added to your Twenty workspace - -- **A "Recording Bot" field on Calendar Events.** A select field (On / Off, On - by default) on every CalendarEvent. Leave it On to record the meeting; switch - it Off to keep the bot out of that specific event. -- **A "Call Recording" tab on the Calendar Event record page.** A viewer with - the meeting's video player and a speaker-attributed, timestamped transcript - that follows along as the recording plays. -- **Call Recording records.** Each recording is stored as a standard - **CallRecording**: the mixed audio (MP3) and video (MP4), the transcript, the - call's actual start and end times, and a lifecycle status (`SCHEDULED` → - `JOINING` → `RECORDING` → `PROCESSING` → `COMPLETED`, or `FAILED`), with a - Meeting Bot Failure Reason when failure details are available. -- **A default role.** A scoped application role that reads calendar events, - participants, people, and workspace members to decide attendance, and writes - the resulting CallRecording records, uploads recording media, and fills - transcripts. It cannot delete records or change settings. - -## How recording works - -- **On by default.** Once an admin installs the app and configures Recall.ai - credentials, every eligible meeting is recorded automatically — there is - nothing each person has to switch on. -- **A meeting is eligible when** it is not canceled, has a conference link, has - not ended yet, and its **Recording Bot** field is On. If any of those isn't - true — the event was canceled, you turned recording Off, there's no video - link, or the meeting has already ended — no bot is scheduled. -- **Opting out of a single meeting.** Open the event and set **Recording Bot** - to Off (or do it from the calendar events list view). The app cancels any bot - it had scheduled for that meeting. -- **Joining and leaving.** By default the bot joins one minute before the start - time and waits in the lobby up to twenty minutes to be admitted. It leaves on - its own if no one ever joins, or shortly after everyone else has left. These - are all tunable — see [Application variables](#application-variables). -- **It tracks the calendar.** If a meeting's time, link, or recording - preference changes, the app reschedules or cancels the bot to match. A - periodic reconciliation job runs as a safety net, keeping recordings and bots - in sync even when a real-time update is missed. - -## Billing - -Recording is a metered feature. Each recording is charged on its **actual call -duration** — from when the bot starts recording to when it stops — prorated, at -a rate of **1 credit per recording-hour** (1,000,000 micro-credits). A meeting -the bot never recorded (opted out, canceled, or no one showed) is not charged. - -## Installing - -1. Open **Settings → Applications** in your Twenty workspace. -2. Find **Twenty Meeting Bot** and click **Install**. -3. A server admin completes the one-time - [Self-hosting setup](#self-hosting-setup-admin-only) below to wire up the - Recall.ai API key and webhook. On Twenty Cloud these may already be - configured. - -## Limitations - -What this app intentionally does **not** do in v1: - -- **Recording is workspace-wide, not per person.** When the app is installed, - every eligible meeting is recorded by default; there is no per-user "record - my meetings" toggle yet. Control is per-meeting via the **Recording Bot** - field. Per-user opt-in/out is planned for a later version. -- **The meeting must be a synced calendar event with a conference link.** The - bot is scheduled from CalendarEvents that Twenty has synced from a connected - calendar (Google / Outlook / CalDAV) and that carry a video-conference link. - Ad-hoc calls that were never on a synced calendar are not recorded. -- **A recording completes only when both its audio and video are ingested.** - Recall produces only the artifacts requested at bot creation (mixed MP3 + - MP4); a recording reaches `COMPLETED` once both have been stored. If - processing fails, it is marked `FAILED`. -- **Recall.ai media is temporary; Twenty's copy is not.** Recall retains the - source media for a limited window (about seven days by default) to stay - inside its free-storage window. Twenty ingests and stores the video, audio, - and transcript in its own storage, so they remain available after Recall's - media expires. - -## Troubleshooting - -| Symptom | Likely cause | Fix | -|---|---|---| -| No bot joined a meeting | **Recording Bot** was Off, the event had no conference link, it wasn't synced from a connected calendar, or `RECALL_API_KEY` isn't set | Confirm the event is On, upcoming, has a video link, and came from a synced calendar; admin: confirm `RECALL_API_KEY` is set | -| Recording never reaches `COMPLETED` | A Recall webhook was missed, or only one of audio/video was produced | The reconciliation job pulls the latest status from Recall within a few minutes; if it is marked `FAILED`, inspect the bot in the Recall dashboard | -| Transcript empty, or marked pending/failed | Recall hasn't finished async transcription yet, or transcription failed for that call | Wait for the reconciliation job to ingest the transcript; a persistent failure leaves a marker in the transcript | -| Webhook rejected with `401` (Recall keeps retrying) | `RECALL_WEBHOOK_SECRET` doesn't match the Recall endpoint's signing secret | Re-copy the `whsec_…` secret from the Recall webhook endpoint into the `RECALL_WEBHOOK_SECRET` server variable | -| Webhook rejected with `500` about the secret | `RECALL_WEBHOOK_SECRET` is not set | Admin: set it on the application registration | -| Bot left almost immediately | No one was admitted before the lobby / no-one-joined timeout, or everyone left | Adjust `MEETING_BOT_WAITING_ROOM_TIMEOUT_SECONDS` / `MEETING_BOT_NOONE_JOINED_TIMEOUT_SECONDS` if too aggressive | -| Bot joined a meeting you didn't want recorded | Recording is on by default | Set the event's **Recording Bot** field to Off; the scheduled bot is canceled | - ---- - -## Self-hosting setup (admin-only) - -This section is for Twenty server admins. If you're on Twenty Cloud, the -credentials may already be configured. - -### Server variables - -Set these on the application registration after installing (Settings → -Applications → Twenty Meeting Bot): - -| Server variable | Required | Purpose | -|---|---|---| -| `RECALL_API_KEY` | Yes | Recall.ai API key for the configured region; used to schedule, update, and cancel bots. | -| `RECALL_REGION` | No | Recall.ai region for API requests. Defaults to `eu-central-1` (Europe / Frankfurt). | -| `MEETING_BOT_RECORDING_RETENTION_HOURS` | No | How long Recall.ai retains the source media after processing. Defaults to `166` hours (6 days 22 hours), just under Recall's 168-hour free-storage window. Values above `168` may incur Recall storage charges. Twenty's ingested copy is unaffected. | -| `RECALL_WEBHOOK_SECRET` | Yes | Svix signing secret (`whsec_…`) used to verify incoming Recall webhooks. | - -### Application variables - -A workspace admin can tune bot behavior through application variables: - -| Application variable | Default | Purpose | -|---|---|---| -| `MEETING_BOT_NAME` | `Twenty Meeting Bot` | Display name the bot uses when it joins a call. | -| `MEETING_BOT_JOIN_EARLY_MINUTES` | `1` | Minutes before the start time the bot joins. Set to `0` to join at the scheduled start. | -| `MEETING_BOT_WAITING_ROOM_TIMEOUT_SECONDS` | `1200` | Seconds the bot waits in the lobby before giving up and leaving. | -| `MEETING_BOT_NOONE_JOINED_TIMEOUT_SECONDS` | `1200` | Seconds the bot stays in an empty meeting when no one else ever joins. | -| `MEETING_BOT_EVERYONE_LEFT_TIMEOUT_SECONDS` | `2` | Seconds the bot keeps recording after everyone else leaves. | - -### Configuring the Recall webhook - -The app exposes a server webhook route that verifies the Recall/Svix signature, -advances the matching CallRecording's lifecycle status (`JOINING` → `RECORDING` -→ `PROCESSING`, or `FAILED`), and — once the recording finishes — -ingests the audio, video, and transcript. It never moves a status backward, so -out-of-order or duplicate deliveries are safe, and it returns a non-2xx response -on signature failures so Recall retries. - -Use this URL on your deployment, replacing only the host: - -```text -https:///webhooks/server/8da4b8b5-5edf-4880-b51f-ab6e679ec617/9215afe6-1497-4149-a49d-e608e239bbaf -``` - -The first ID is the **Twenty Meeting Bot application registration**. The second -ID is the **Recall webhook logic function**. - -1. In the Recall.ai dashboard, create a webhook endpoint pointing at your - deployment's webhook URL, subscribed to the **bot status-change**, - **recording**, and **transcript** events (`bot.status_change`, - `recording.done`, `recording.failed`, `transcript.done`, - `transcript.failed`). Status-change drives the lifecycle; the recording and - transcript events trigger media and transcript ingestion. Subscribing to - status changes alone leaves ingestion to the reconciliation backstop. -2. Copy the endpoint's signing secret — it starts with `whsec_`. -3. Set it as the `RECALL_WEBHOOK_SECRET` server variable on the **Twenty - Meeting Bot** application registration. -4. Set `RECALL_API_KEY` (and optionally `RECALL_REGION`) the same way. diff --git a/packages/twenty-apps/public/twenty-meeting-bot/package.json b/packages/twenty-apps/public/twenty-meeting-bot/package.json deleted file mode 100644 index 8316af39e0719..0000000000000 --- a/packages/twenty-apps/public/twenty-meeting-bot/package.json +++ /dev/null @@ -1,40 +0,0 @@ -{ - "name": "twenty-meeting-bot", - "version": "0.1.4", - "license": "MIT", - "engines": { - "node": "^24.5.0", - "npm": "please-use-yarn", - "yarn": ">=4.0.2" - }, - "keywords": [], - "packageManager": "yarn@4.9.2", - "scripts": { - "twenty": "twenty", - "lint": "oxlint -c .oxlintrc.json .", - "lint:fix": "oxlint --fix -c .oxlintrc.json .", - "typecheck": "tsgo --noEmit -p tsconfig.spec.json", - "test": "vitest run", - "test:unit": "vitest run --config vitest.unit.config.ts", - "test:unit:watch": "vitest --config vitest.unit.config.ts", - "test:watch": "vitest" - }, - "dependencies": { - "@sniptt/guards": "^0.2.0" - }, - "devDependencies": { - "@emotion/react": "^11.14.0", - "@emotion/styled": "^11.14.0", - "@types/node": "^24.7.2", - "@types/react": "^19.0.0", - "@typescript/native-preview": "^7.0.0-dev.20260116.1", - "oxlint": "^0.16.0", - "react": "^19.0.0", - "react-dom": "^19.0.0", - "twenty-client-sdk": "2.15.0", - "twenty-sdk": "2.15.0", - "typescript": "^5.9.3", - "vite-tsconfig-paths": "^4.2.1", - "vitest": "^4.1.9" - } -} diff --git a/packages/twenty-apps/public/twenty-meeting-bot/public/gallery/twenty-meeting-bot-cover.png b/packages/twenty-apps/public/twenty-meeting-bot/public/gallery/twenty-meeting-bot-cover.png deleted file mode 100644 index 41b463347b416..0000000000000 Binary files a/packages/twenty-apps/public/twenty-meeting-bot/public/gallery/twenty-meeting-bot-cover.png and /dev/null differ diff --git a/packages/twenty-apps/public/twenty-meeting-bot/public/logo.svg b/packages/twenty-apps/public/twenty-meeting-bot/public/logo.svg deleted file mode 100644 index a8bed68a83d0f..0000000000000 --- a/packages/twenty-apps/public/twenty-meeting-bot/public/logo.svg +++ /dev/null @@ -1,5 +0,0 @@ - - - - - diff --git a/packages/twenty-apps/public/twenty-meeting-bot/src/__tests__/global-setup.ts b/packages/twenty-apps/public/twenty-meeting-bot/src/__tests__/global-setup.ts deleted file mode 100644 index 5d6782a1ce972..0000000000000 --- a/packages/twenty-apps/public/twenty-meeting-bot/src/__tests__/global-setup.ts +++ /dev/null @@ -1,100 +0,0 @@ -import * as fs from 'fs'; -import * as os from 'os'; -import * as path from 'path'; - -import { appDevOnce, appUninstall } from 'twenty-sdk/cli'; - -const APP_PATH = process.cwd(); -const CONFIG_DIR = path.join(os.homedir(), '.twenty'); -const CONFIG_PATH = path.join(CONFIG_DIR, 'config.test.json'); - -function validateEnv(): { apiUrl: string; apiKey: string } { - const apiUrl = process.env.TWENTY_API_URL; - const apiKey = process.env.TWENTY_API_KEY; - - if (!apiUrl || !apiKey) { - throw new Error( - 'TWENTY_API_URL and TWENTY_API_KEY must be set.\n' + - 'Start a local server: yarn twenty docker:start\n' + - 'Or set them in vitest env config.', - ); - } - - return { apiUrl, apiKey }; -} - -async function checkServer(apiUrl: string) { - let response: Response; - - try { - response = await fetch(`${apiUrl}/healthz`); - } catch { - throw new Error( - `Twenty server is not reachable at ${apiUrl}. ` + - 'Make sure the server is running before executing integration tests.', - ); - } - - if (!response.ok) { - throw new Error(`Server at ${apiUrl} returned ${response.status}`); - } -} - -function writeConfig(apiUrl: string, apiKey: string) { - const payload = JSON.stringify( - { - remotes: { - local: { apiUrl, apiKey, accessToken: apiKey }, - }, - defaultRemote: 'local', - }, - null, - 2, - ); - - fs.mkdirSync(CONFIG_DIR, { recursive: true }); - fs.writeFileSync(CONFIG_PATH, payload); -} - -function removeConfig() { - fs.rmSync(CONFIG_PATH, { force: true }); -} - -export async function setup() { - const { apiUrl, apiKey } = validateEnv(); - - await checkServer(apiUrl); - - writeConfig(apiUrl, apiKey); - - await appUninstall({ appPath: APP_PATH }).catch(() => {}); - - const result = await appDevOnce({ - appPath: APP_PATH, - onProgress: (message: string) => console.log(`[dev] ${message}`), - }); - - if (!result.success) { - throw new Error( - `Dev sync failed: ${result.error?.message ?? 'Unknown error'}`, - ); - } -} - -export async function teardown() { - try { - const uninstallResult = await appUninstall({ appPath: APP_PATH }); - - if (!uninstallResult.success) { - console.warn( - `App uninstall failed: ${uninstallResult.error?.message ?? 'Unknown error'}`, - ); - } - } catch (error) { - console.warn( - `App uninstall failed: ${error instanceof Error ? error.message : String(error)}`, - ); - } finally { - removeConfig(); - } -} diff --git a/packages/twenty-apps/public/twenty-meeting-bot/src/__tests__/schema.integration-test.ts b/packages/twenty-apps/public/twenty-meeting-bot/src/__tests__/schema.integration-test.ts deleted file mode 100644 index b9623320cf212..0000000000000 --- a/packages/twenty-apps/public/twenty-meeting-bot/src/__tests__/schema.integration-test.ts +++ /dev/null @@ -1,123 +0,0 @@ -import { CoreApiClient } from 'twenty-client-sdk/core'; -import { MetadataApiClient } from 'twenty-client-sdk/metadata'; -import { describe, expect, it } from 'vitest'; - -import { APPLICATION_UNIVERSAL_IDENTIFIER } from 'src/constants/application-universal-identifier'; -import { CallRecordingRequestStatus } from 'src/logic-functions/constants/call-recording-request-status'; -import { CallRecordingStatus } from 'src/logic-functions/constants/call-recording-status'; -import { - executeCurrentSchemaMutation, - type CurrentSchemaUpdateCallRecordingMutation, -} from 'src/logic-functions/data/execute-current-schema-mutation.util'; - -describe('App installation', () => { - it('should find the installed app in the applications list', async () => { - const client = new MetadataApiClient(); - - const result = await client.query({ - findManyApplications: { - id: true, - name: true, - universalIdentifier: true, - }, - }); - - const app = result.findManyApplications.find( - (a: { universalIdentifier: string }) => - a.universalIdentifier === APPLICATION_UNIVERSAL_IDENTIFIER, - ); - - expect(app).toBeDefined(); - }); -}); - -describe('CallRecording status contract', () => { - it('accepts every app status supported by the current server and every request status value the app mirrors', async () => { - const client = new CoreApiClient(); - const serverCallRecordingStatuses = await getServerCallRecordingStatuses(); - - const created = await client.mutation({ - createCallRecording: { - __args: { - data: { - title: 'Integration test recording', - status: CallRecordingStatus.SCHEDULED, - recordingRequestStatus: CallRecordingRequestStatus.REQUESTED, - }, - }, - id: true, - }, - }); - - const callRecordingId = created.createCallRecording?.id; - - expect(callRecordingId).toBeDefined(); - - if (callRecordingId === undefined) { - throw new Error('Expected call recording creation to return an id'); - } - - expect(serverCallRecordingStatuses).toEqual( - expect.arrayContaining([ - CallRecordingStatus.SCHEDULED, - CallRecordingStatus.JOINING, - CallRecordingStatus.RECORDING, - CallRecordingStatus.PROCESSING, - CallRecordingStatus.COMPLETED, - ]), - ); - - // TODO: Remove this compatibility filter once the released server/SDK - // exposes FAILED instead of FAILED_UNKNOWN. - const statusesAcceptedByCurrentServer = Object.values( - CallRecordingStatus, - ).filter((status) => serverCallRecordingStatuses.includes(status)); - - for (const status of statusesAcceptedByCurrentServer) { - const mutation = { - updateCallRecording: { - __args: { id: callRecordingId, data: { status } }, - status: true, - }, - } satisfies CurrentSchemaUpdateCallRecordingMutation; - - const updated = await executeCurrentSchemaMutation(client, mutation); - - expect(updated.updateCallRecording?.status).toBe(status); - } - - for (const recordingRequestStatus of Object.values( - CallRecordingRequestStatus, - )) { - const updated = await client.mutation({ - updateCallRecording: { - __args: { id: callRecordingId, data: { recordingRequestStatus } }, - recordingRequestStatus: true, - }, - }); - - expect(updated.updateCallRecording?.recordingRequestStatus).toBe( - recordingRequestStatus, - ); - } - - await client.mutation({ - destroyCallRecording: { - __args: { id: callRecordingId }, - id: true, - }, - }); - }); -}); - -type GeneratedCoreSchemaRuntime = { - enumCallRecordingStatusEnum: Record; -}; - -const getServerCallRecordingStatuses = async (): Promise => { - const generatedCoreSchema = (await import( - 'twenty-client-sdk/core' - )) as unknown as GeneratedCoreSchemaRuntime; - - return Object.values(generatedCoreSchema.enumCallRecordingStatusEnum); -}; diff --git a/packages/twenty-apps/public/twenty-meeting-bot/src/application-config.ts b/packages/twenty-apps/public/twenty-meeting-bot/src/application-config.ts deleted file mode 100644 index 0ecf95e8a869b..0000000000000 --- a/packages/twenty-apps/public/twenty-meeting-bot/src/application-config.ts +++ /dev/null @@ -1,96 +0,0 @@ -import { defineApplication } from 'twenty-sdk/define'; - -import { APP_DESCRIPTION } from 'src/constants/app-description'; -import { APP_DISPLAY_NAME } from 'src/constants/app-display-name'; -import { APPLICATION_UNIVERSAL_IDENTIFIER } from 'src/constants/application-universal-identifier'; -import { MEETING_BOT_EVERYONE_LEFT_TIMEOUT_SECONDS_APP_VARIABLE_UNIVERSAL_IDENTIFIER } from 'src/constants/meeting-bot-everyone-left-timeout-seconds-app-variable-universal-identifier'; -import { MEETING_BOT_JOIN_EARLY_MINUTES_APP_VARIABLE_UNIVERSAL_IDENTIFIER } from 'src/constants/meeting-bot-join-early-minutes-app-variable-universal-identifier'; -import { MEETING_BOT_NAME_APP_VARIABLE_UNIVERSAL_IDENTIFIER } from 'src/constants/meeting-bot-name-app-variable-universal-identifier'; -import { MEETING_BOT_NOONE_JOINED_TIMEOUT_SECONDS_APP_VARIABLE_UNIVERSAL_IDENTIFIER } from 'src/constants/meeting-bot-noone-joined-timeout-seconds-app-variable-universal-identifier'; -import { MEETING_BOT_WAITING_ROOM_TIMEOUT_SECONDS_APP_VARIABLE_UNIVERSAL_IDENTIFIER } from 'src/constants/meeting-bot-waiting-room-timeout-seconds-app-variable-universal-identifier'; -import { DEFAULT_MEETING_BOT_JOIN_EARLY_MINUTES } from 'src/logic-functions/constants/default-meeting-bot-join-early-minutes'; -import { DEFAULT_MEETING_BOT_NAME } from 'src/logic-functions/constants/default-meeting-bot-name'; -import { DEFAULT_MEETING_BOT_RECORDING_RETENTION_HOURS } from 'src/logic-functions/constants/default-meeting-bot-recording-retention-hours'; -import { DEFAULT_RECALL_REGION } from 'src/logic-functions/constants/default-recall-region'; -import { MEETING_BOT_EVERYONE_LEFT_TIMEOUT_SECONDS } from 'src/logic-functions/constants/meeting-bot-everyone-left-timeout-seconds'; -import { MEETING_BOT_EVERYONE_LEFT_TIMEOUT_SECONDS_ENV_VAR_NAME } from 'src/logic-functions/constants/meeting-bot-everyone-left-timeout-seconds-env-var-name'; -import { MEETING_BOT_JOIN_EARLY_MINUTES_ENV_VAR_NAME } from 'src/logic-functions/constants/meeting-bot-join-early-minutes-env-var-name'; -import { MEETING_BOT_NAME_ENV_VAR_NAME } from 'src/logic-functions/constants/meeting-bot-name-env-var-name'; -import { MEETING_BOT_NOONE_JOINED_TIMEOUT_SECONDS } from 'src/logic-functions/constants/meeting-bot-noone-joined-timeout-seconds'; -import { MEETING_BOT_NOONE_JOINED_TIMEOUT_SECONDS_ENV_VAR_NAME } from 'src/logic-functions/constants/meeting-bot-noone-joined-timeout-seconds-env-var-name'; -import { MEETING_BOT_RECORDING_RETENTION_HOURS_ENV_VAR_NAME } from 'src/logic-functions/constants/meeting-bot-recording-retention-hours-env-var-name'; -import { MEETING_BOT_WAITING_ROOM_TIMEOUT_SECONDS } from 'src/logic-functions/constants/meeting-bot-waiting-room-timeout-seconds'; -import { MEETING_BOT_WAITING_ROOM_TIMEOUT_SECONDS_ENV_VAR_NAME } from 'src/logic-functions/constants/meeting-bot-waiting-room-timeout-seconds-env-var-name'; -import { RECALL_API_KEY_ENV_VAR_NAME } from 'src/logic-functions/constants/recall-api-key-env-var-name'; -import { RECALL_REGION_ENV_VAR_NAME } from 'src/logic-functions/constants/recall-region-env-var-name'; -import { RECALL_WEBHOOK_SECRET_ENV_VAR_NAME } from 'src/logic-functions/constants/recall-webhook-secret-env-var-name'; - -export default defineApplication({ - universalIdentifier: APPLICATION_UNIVERSAL_IDENTIFIER, - displayName: APP_DISPLAY_NAME, - description: APP_DESCRIPTION, - logoUrl: 'public/logo.svg', - screenshots: ['public/gallery/twenty-meeting-bot-cover.png'], - applicationVariables: { - [MEETING_BOT_NAME_ENV_VAR_NAME]: { - universalIdentifier: MEETING_BOT_NAME_APP_VARIABLE_UNIVERSAL_IDENTIFIER, - description: 'Display name the meeting bot uses when it joins a call.', - isSecret: false, - value: DEFAULT_MEETING_BOT_NAME, - }, - [MEETING_BOT_JOIN_EARLY_MINUTES_ENV_VAR_NAME]: { - universalIdentifier: - MEETING_BOT_JOIN_EARLY_MINUTES_APP_VARIABLE_UNIVERSAL_IDENTIFIER, - description: - 'How many minutes before the meeting start time the bot should join. Set to 0 to join at the scheduled start time.', - isSecret: false, - value: String(DEFAULT_MEETING_BOT_JOIN_EARLY_MINUTES), - }, - [MEETING_BOT_WAITING_ROOM_TIMEOUT_SECONDS_ENV_VAR_NAME]: { - universalIdentifier: - MEETING_BOT_WAITING_ROOM_TIMEOUT_SECONDS_APP_VARIABLE_UNIVERSAL_IDENTIFIER, - description: - 'How many seconds the bot waits in a meeting lobby before giving up and leaving.', - isSecret: false, - value: String(MEETING_BOT_WAITING_ROOM_TIMEOUT_SECONDS), - }, - [MEETING_BOT_NOONE_JOINED_TIMEOUT_SECONDS_ENV_VAR_NAME]: { - universalIdentifier: - MEETING_BOT_NOONE_JOINED_TIMEOUT_SECONDS_APP_VARIABLE_UNIVERSAL_IDENTIFIER, - description: - 'How many seconds the bot stays in an empty meeting when no one else ever joins.', - isSecret: false, - value: String(MEETING_BOT_NOONE_JOINED_TIMEOUT_SECONDS), - }, - [MEETING_BOT_EVERYONE_LEFT_TIMEOUT_SECONDS_ENV_VAR_NAME]: { - universalIdentifier: - MEETING_BOT_EVERYONE_LEFT_TIMEOUT_SECONDS_APP_VARIABLE_UNIVERSAL_IDENTIFIER, - description: - 'How many seconds the bot keeps recording after everyone else leaves the meeting.', - isSecret: false, - value: String(MEETING_BOT_EVERYONE_LEFT_TIMEOUT_SECONDS), - }, - }, - serverVariables: { - [RECALL_API_KEY_ENV_VAR_NAME]: { - description: - 'Recall.ai API key for the configured region. Set by the server admin on this registration after installation; used to create, update, and cancel scheduled meeting bots.', - isSecret: true, - isRequired: true, - }, - [RECALL_REGION_ENV_VAR_NAME]: { - description: `Recall.ai region used for API requests. Defaults to ${DEFAULT_RECALL_REGION} when unset. Europe Frankfurt is eu-central-1.`, - isSecret: false, - }, - [MEETING_BOT_RECORDING_RETENTION_HOURS_ENV_VAR_NAME]: { - description: `How many hours Recall.ai retains recording media after processing. Defaults to ${DEFAULT_MEETING_BOT_RECORDING_RETENTION_HOURS} hours (6 days and 22 hours) to stay below Recall.ai's 7-day free storage window. Values above 168 hours may incur Recall.ai storage charges.`, - isSecret: false, - }, - [RECALL_WEBHOOK_SECRET_ENV_VAR_NAME]: { - description: - 'Recall.ai webhook signing secret (whsec_...). Set by the server admin from the Recall webhook endpoint settings; used to verify the Svix signature of incoming Recall webhook deliveries.', - isSecret: true, - isRequired: true, - }, - }, -}); diff --git a/packages/twenty-apps/public/twenty-meeting-bot/src/constants/__tests__/call-recording-field-universal-identifiers.test.ts b/packages/twenty-apps/public/twenty-meeting-bot/src/constants/__tests__/call-recording-field-universal-identifiers.test.ts deleted file mode 100644 index 78d6c1b79987f..0000000000000 --- a/packages/twenty-apps/public/twenty-meeting-bot/src/constants/__tests__/call-recording-field-universal-identifiers.test.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS } from 'twenty-sdk/define'; -import { describe, expect, it } from 'vitest'; - -import { CALL_RECORDING_AUDIO_FIELD_UNIVERSAL_IDENTIFIER } from 'src/constants/call-recording-audio-field-universal-identifier'; -import { CALL_RECORDING_VIDEO_FIELD_UNIVERSAL_IDENTIFIER } from 'src/constants/call-recording-video-field-universal-identifier'; - -// This test is nothing more than a sanity check to ensure that the universal identifiers for the call recording media fields are correct. -describe('call recording field universal identifiers', () => { - it('matches the standard CallRecording media field identifiers', () => { - expect(CALL_RECORDING_AUDIO_FIELD_UNIVERSAL_IDENTIFIER).toBe( - STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.callRecording.fields.audio - .universalIdentifier, - ); - expect(CALL_RECORDING_VIDEO_FIELD_UNIVERSAL_IDENTIFIER).toBe( - STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.callRecording.fields.video - .universalIdentifier, - ); - }); -}); diff --git a/packages/twenty-apps/public/twenty-meeting-bot/src/constants/app-description.ts b/packages/twenty-apps/public/twenty-meeting-bot/src/constants/app-description.ts deleted file mode 100644 index e5580e7caeaf7..0000000000000 --- a/packages/twenty-apps/public/twenty-meeting-bot/src/constants/app-description.ts +++ /dev/null @@ -1,2 +0,0 @@ -export const APP_DESCRIPTION = - 'Capture every customer conversation automatically. A meeting bot joins eligible meetings and records calls for you.'; diff --git a/packages/twenty-apps/public/twenty-meeting-bot/src/constants/app-display-name.ts b/packages/twenty-apps/public/twenty-meeting-bot/src/constants/app-display-name.ts deleted file mode 100644 index 9208fcde477d6..0000000000000 --- a/packages/twenty-apps/public/twenty-meeting-bot/src/constants/app-display-name.ts +++ /dev/null @@ -1 +0,0 @@ -export const APP_DISPLAY_NAME = 'Twenty Meeting Bot'; diff --git a/packages/twenty-apps/public/twenty-meeting-bot/src/constants/application-universal-identifier.ts b/packages/twenty-apps/public/twenty-meeting-bot/src/constants/application-universal-identifier.ts deleted file mode 100644 index b88164429bd37..0000000000000 --- a/packages/twenty-apps/public/twenty-meeting-bot/src/constants/application-universal-identifier.ts +++ /dev/null @@ -1,2 +0,0 @@ -export const APPLICATION_UNIVERSAL_IDENTIFIER = - '8da4b8b5-5edf-4880-b51f-ab6e679ec617'; diff --git a/packages/twenty-apps/public/twenty-meeting-bot/src/constants/calendar-event-reconciliation-logic-function-universal-identifier.ts b/packages/twenty-apps/public/twenty-meeting-bot/src/constants/calendar-event-reconciliation-logic-function-universal-identifier.ts deleted file mode 100644 index b9f7a2ee81302..0000000000000 --- a/packages/twenty-apps/public/twenty-meeting-bot/src/constants/calendar-event-reconciliation-logic-function-universal-identifier.ts +++ /dev/null @@ -1,2 +0,0 @@ -export const CALENDAR_EVENT_RECONCILIATION_LOGIC_FUNCTION_UNIVERSAL_IDENTIFIER = - '1f28c477-6423-4911-85bf-2296ef112be9'; diff --git a/packages/twenty-apps/public/twenty-meeting-bot/src/constants/calendar-event-record-page-layout-universal-identifier.ts b/packages/twenty-apps/public/twenty-meeting-bot/src/constants/calendar-event-record-page-layout-universal-identifier.ts deleted file mode 100644 index a66d22a24f0d2..0000000000000 --- a/packages/twenty-apps/public/twenty-meeting-bot/src/constants/calendar-event-record-page-layout-universal-identifier.ts +++ /dev/null @@ -1,2 +0,0 @@ -export const CALENDAR_EVENT_RECORD_PAGE_LAYOUT_UNIVERSAL_IDENTIFIER = - 'b9b10e40-9ce2-4704-8ac6-c6e92e2563c1'; diff --git a/packages/twenty-apps/public/twenty-meeting-bot/src/constants/calendar-event-recording-front-component-universal-identifier.ts b/packages/twenty-apps/public/twenty-meeting-bot/src/constants/calendar-event-recording-front-component-universal-identifier.ts deleted file mode 100644 index 3fe07b41b8084..0000000000000 --- a/packages/twenty-apps/public/twenty-meeting-bot/src/constants/calendar-event-recording-front-component-universal-identifier.ts +++ /dev/null @@ -1,2 +0,0 @@ -export const CALENDAR_EVENT_RECORDING_FRONT_COMPONENT_UNIVERSAL_IDENTIFIER = - '445d742e-1c27-46ee-a9ab-4a1dde65adcf'; diff --git a/packages/twenty-apps/public/twenty-meeting-bot/src/constants/calendar-event-recording-page-layout-tab-universal-identifier.ts b/packages/twenty-apps/public/twenty-meeting-bot/src/constants/calendar-event-recording-page-layout-tab-universal-identifier.ts deleted file mode 100644 index c559deebca8ba..0000000000000 --- a/packages/twenty-apps/public/twenty-meeting-bot/src/constants/calendar-event-recording-page-layout-tab-universal-identifier.ts +++ /dev/null @@ -1,2 +0,0 @@ -export const CALENDAR_EVENT_RECORDING_PAGE_LAYOUT_TAB_UNIVERSAL_IDENTIFIER = - '10c2c22d-952b-42ea-81fe-2e93e7d30f86'; diff --git a/packages/twenty-apps/public/twenty-meeting-bot/src/constants/calendar-event-recording-page-layout-widget-universal-identifier.ts b/packages/twenty-apps/public/twenty-meeting-bot/src/constants/calendar-event-recording-page-layout-widget-universal-identifier.ts deleted file mode 100644 index 1588ffe3b72a0..0000000000000 --- a/packages/twenty-apps/public/twenty-meeting-bot/src/constants/calendar-event-recording-page-layout-widget-universal-identifier.ts +++ /dev/null @@ -1,2 +0,0 @@ -export const CALENDAR_EVENT_RECORDING_PAGE_LAYOUT_WIDGET_UNIVERSAL_IDENTIFIER = - '29c2315b-2ee6-41df-91b7-42f58f6c1a53'; diff --git a/packages/twenty-apps/public/twenty-meeting-bot/src/constants/call-recording-audio-field-universal-identifier.ts b/packages/twenty-apps/public/twenty-meeting-bot/src/constants/call-recording-audio-field-universal-identifier.ts deleted file mode 100644 index 6d17dc45d95c8..0000000000000 --- a/packages/twenty-apps/public/twenty-meeting-bot/src/constants/call-recording-audio-field-universal-identifier.ts +++ /dev/null @@ -1,2 +0,0 @@ -export const CALL_RECORDING_AUDIO_FIELD_UNIVERSAL_IDENTIFIER = - '2eafc2d0-8fec-430c-a939-65ca5fbc0f08'; diff --git a/packages/twenty-apps/public/twenty-meeting-bot/src/constants/call-recording-video-field-universal-identifier.ts b/packages/twenty-apps/public/twenty-meeting-bot/src/constants/call-recording-video-field-universal-identifier.ts deleted file mode 100644 index ff4ebcf5fa308..0000000000000 --- a/packages/twenty-apps/public/twenty-meeting-bot/src/constants/call-recording-video-field-universal-identifier.ts +++ /dev/null @@ -1,2 +0,0 @@ -export const CALL_RECORDING_VIDEO_FIELD_UNIVERSAL_IDENTIFIER = - 'bb9523d3-457e-4f4b-8c79-27a77afb87da'; diff --git a/packages/twenty-apps/public/twenty-meeting-bot/src/constants/default-role-universal-identifier.ts b/packages/twenty-apps/public/twenty-meeting-bot/src/constants/default-role-universal-identifier.ts deleted file mode 100644 index 1c23dd519d96e..0000000000000 --- a/packages/twenty-apps/public/twenty-meeting-bot/src/constants/default-role-universal-identifier.ts +++ /dev/null @@ -1,2 +0,0 @@ -export const DEFAULT_ROLE_UNIVERSAL_IDENTIFIER = - '5fcf4d3a-0aca-42d9-9beb-7387f43ec180'; diff --git a/packages/twenty-apps/public/twenty-meeting-bot/src/constants/meeting-bot-everyone-left-timeout-seconds-app-variable-universal-identifier.ts b/packages/twenty-apps/public/twenty-meeting-bot/src/constants/meeting-bot-everyone-left-timeout-seconds-app-variable-universal-identifier.ts deleted file mode 100644 index 6197b972b7bf1..0000000000000 --- a/packages/twenty-apps/public/twenty-meeting-bot/src/constants/meeting-bot-everyone-left-timeout-seconds-app-variable-universal-identifier.ts +++ /dev/null @@ -1,2 +0,0 @@ -export const MEETING_BOT_EVERYONE_LEFT_TIMEOUT_SECONDS_APP_VARIABLE_UNIVERSAL_IDENTIFIER = - 'c866ddd4-fb7b-4cb4-8ad1-5599755e495c'; diff --git a/packages/twenty-apps/public/twenty-meeting-bot/src/constants/meeting-bot-failure-reason-on-call-recording-field-universal-identifier.ts b/packages/twenty-apps/public/twenty-meeting-bot/src/constants/meeting-bot-failure-reason-on-call-recording-field-universal-identifier.ts deleted file mode 100644 index 8de20ef3a7da3..0000000000000 --- a/packages/twenty-apps/public/twenty-meeting-bot/src/constants/meeting-bot-failure-reason-on-call-recording-field-universal-identifier.ts +++ /dev/null @@ -1,2 +0,0 @@ -export const MEETING_BOT_FAILURE_REASON_ON_CALL_RECORDING_FIELD_UNIVERSAL_IDENTIFIER = - '33a577e4-02f5-48bd-8bed-d365949caa72'; diff --git a/packages/twenty-apps/public/twenty-meeting-bot/src/constants/meeting-bot-join-early-minutes-app-variable-universal-identifier.ts b/packages/twenty-apps/public/twenty-meeting-bot/src/constants/meeting-bot-join-early-minutes-app-variable-universal-identifier.ts deleted file mode 100644 index 3d4165ec0e32a..0000000000000 --- a/packages/twenty-apps/public/twenty-meeting-bot/src/constants/meeting-bot-join-early-minutes-app-variable-universal-identifier.ts +++ /dev/null @@ -1,2 +0,0 @@ -export const MEETING_BOT_JOIN_EARLY_MINUTES_APP_VARIABLE_UNIVERSAL_IDENTIFIER = - '0568ebb2-3f64-47de-8c0d-d367dfbb7462'; diff --git a/packages/twenty-apps/public/twenty-meeting-bot/src/constants/meeting-bot-name-app-variable-universal-identifier.ts b/packages/twenty-apps/public/twenty-meeting-bot/src/constants/meeting-bot-name-app-variable-universal-identifier.ts deleted file mode 100644 index 51a8ac9177ee0..0000000000000 --- a/packages/twenty-apps/public/twenty-meeting-bot/src/constants/meeting-bot-name-app-variable-universal-identifier.ts +++ /dev/null @@ -1,2 +0,0 @@ -export const MEETING_BOT_NAME_APP_VARIABLE_UNIVERSAL_IDENTIFIER = - 'c54cbacd-ad10-40b4-9056-7aaf23846d64'; diff --git a/packages/twenty-apps/public/twenty-meeting-bot/src/constants/meeting-bot-noone-joined-timeout-seconds-app-variable-universal-identifier.ts b/packages/twenty-apps/public/twenty-meeting-bot/src/constants/meeting-bot-noone-joined-timeout-seconds-app-variable-universal-identifier.ts deleted file mode 100644 index 60b8a04a009b7..0000000000000 --- a/packages/twenty-apps/public/twenty-meeting-bot/src/constants/meeting-bot-noone-joined-timeout-seconds-app-variable-universal-identifier.ts +++ /dev/null @@ -1,2 +0,0 @@ -export const MEETING_BOT_NOONE_JOINED_TIMEOUT_SECONDS_APP_VARIABLE_UNIVERSAL_IDENTIFIER = - '241180e8-d864-4160-ad02-db44a9e8d395'; diff --git a/packages/twenty-apps/public/twenty-meeting-bot/src/constants/meeting-bot-preference-off-option-id.ts b/packages/twenty-apps/public/twenty-meeting-bot/src/constants/meeting-bot-preference-off-option-id.ts deleted file mode 100644 index 7a51ea4c2f7f5..0000000000000 --- a/packages/twenty-apps/public/twenty-meeting-bot/src/constants/meeting-bot-preference-off-option-id.ts +++ /dev/null @@ -1,2 +0,0 @@ -export const MEETING_BOT_PREFERENCE_OFF_OPTION_ID = - 'cc7de62a-08b6-46c8-aa69-f8117e7dd722'; diff --git a/packages/twenty-apps/public/twenty-meeting-bot/src/constants/meeting-bot-preference-on-calendar-event-field-universal-identifier.ts b/packages/twenty-apps/public/twenty-meeting-bot/src/constants/meeting-bot-preference-on-calendar-event-field-universal-identifier.ts deleted file mode 100644 index c321578654d86..0000000000000 --- a/packages/twenty-apps/public/twenty-meeting-bot/src/constants/meeting-bot-preference-on-calendar-event-field-universal-identifier.ts +++ /dev/null @@ -1,2 +0,0 @@ -export const MEETING_BOT_PREFERENCE_ON_CALENDAR_EVENT_FIELD_UNIVERSAL_IDENTIFIER = - '8ee9444a-2437-4def-8e61-6e493862a4fd'; diff --git a/packages/twenty-apps/public/twenty-meeting-bot/src/constants/meeting-bot-preference-on-calendar-event-view-field-universal-identifier.ts b/packages/twenty-apps/public/twenty-meeting-bot/src/constants/meeting-bot-preference-on-calendar-event-view-field-universal-identifier.ts deleted file mode 100644 index 477d5abd6b3b5..0000000000000 --- a/packages/twenty-apps/public/twenty-meeting-bot/src/constants/meeting-bot-preference-on-calendar-event-view-field-universal-identifier.ts +++ /dev/null @@ -1,2 +0,0 @@ -export const MEETING_BOT_PREFERENCE_ON_CALENDAR_EVENT_VIEW_FIELD_UNIVERSAL_IDENTIFIER = - 'e8c7e9c5-2b4f-4a75-ae46-ea331809106c'; diff --git a/packages/twenty-apps/public/twenty-meeting-bot/src/constants/meeting-bot-preference-on-option-id.ts b/packages/twenty-apps/public/twenty-meeting-bot/src/constants/meeting-bot-preference-on-option-id.ts deleted file mode 100644 index 77a82fa86d48d..0000000000000 --- a/packages/twenty-apps/public/twenty-meeting-bot/src/constants/meeting-bot-preference-on-option-id.ts +++ /dev/null @@ -1,2 +0,0 @@ -export const MEETING_BOT_PREFERENCE_ON_OPTION_ID = - '72431216-49c4-47c8-99af-de4c3831b0be'; diff --git a/packages/twenty-apps/public/twenty-meeting-bot/src/constants/meeting-bot-preference.ts b/packages/twenty-apps/public/twenty-meeting-bot/src/constants/meeting-bot-preference.ts deleted file mode 100644 index 670e71c935c40..0000000000000 --- a/packages/twenty-apps/public/twenty-meeting-bot/src/constants/meeting-bot-preference.ts +++ /dev/null @@ -1,4 +0,0 @@ -export enum MeetingBotPreference { - ON = 'ON', - OFF = 'OFF', -} diff --git a/packages/twenty-apps/public/twenty-meeting-bot/src/constants/meeting-bot-waiting-room-timeout-seconds-app-variable-universal-identifier.ts b/packages/twenty-apps/public/twenty-meeting-bot/src/constants/meeting-bot-waiting-room-timeout-seconds-app-variable-universal-identifier.ts deleted file mode 100644 index bec5abdce4154..0000000000000 --- a/packages/twenty-apps/public/twenty-meeting-bot/src/constants/meeting-bot-waiting-room-timeout-seconds-app-variable-universal-identifier.ts +++ /dev/null @@ -1,2 +0,0 @@ -export const MEETING_BOT_WAITING_ROOM_TIMEOUT_SECONDS_APP_VARIABLE_UNIVERSAL_IDENTIFIER = - '12e4e14d-d539-4d07-b477-4773539dd20b'; diff --git a/packages/twenty-apps/public/twenty-meeting-bot/src/constants/recall-webhook-logic-function-universal-identifier.ts b/packages/twenty-apps/public/twenty-meeting-bot/src/constants/recall-webhook-logic-function-universal-identifier.ts deleted file mode 100644 index 98e5e559bea4e..0000000000000 --- a/packages/twenty-apps/public/twenty-meeting-bot/src/constants/recall-webhook-logic-function-universal-identifier.ts +++ /dev/null @@ -1,2 +0,0 @@ -export const RECALL_WEBHOOK_LOGIC_FUNCTION_UNIVERSAL_IDENTIFIER = - '9215afe6-1497-4149-a49d-e608e239bbaf'; diff --git a/packages/twenty-apps/public/twenty-meeting-bot/src/constants/stale-bot-state-logic-function-universal-identifier.ts b/packages/twenty-apps/public/twenty-meeting-bot/src/constants/stale-bot-state-logic-function-universal-identifier.ts deleted file mode 100644 index 79ea9ad293c95..0000000000000 --- a/packages/twenty-apps/public/twenty-meeting-bot/src/constants/stale-bot-state-logic-function-universal-identifier.ts +++ /dev/null @@ -1,2 +0,0 @@ -export const STALE_BOT_STATE_LOGIC_FUNCTION_UNIVERSAL_IDENTIFIER = - 'e362aa9b-52c6-4b7e-bb20-927e0e8d7cbe'; diff --git a/packages/twenty-apps/public/twenty-meeting-bot/src/default-role.ts b/packages/twenty-apps/public/twenty-meeting-bot/src/default-role.ts deleted file mode 100644 index 62df301574b60..0000000000000 --- a/packages/twenty-apps/public/twenty-meeting-bot/src/default-role.ts +++ /dev/null @@ -1,69 +0,0 @@ -import { - STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS, - SystemPermissionFlag, - defineApplicationRole, -} from 'twenty-sdk/define'; - -import { APP_DISPLAY_NAME } from 'src/constants/app-display-name'; -import { DEFAULT_ROLE_UNIVERSAL_IDENTIFIER } from 'src/constants/default-role-universal-identifier'; - -export default defineApplicationRole({ - universalIdentifier: DEFAULT_ROLE_UNIVERSAL_IDENTIFIER, - label: `${APP_DISPLAY_NAME} default role`, - description: - 'Reads calendar events to decide whether the meeting bot should attend a meeting; writes the resulting CallRecording records, uploads recording media, and fills transcripts.', - canReadAllObjectRecords: false, - canUpdateAllObjectRecords: false, - canSoftDeleteAllObjectRecords: false, - canDestroyAllObjectRecords: false, - canUpdateAllSettings: false, - canBeAssignedToAgents: false, - canBeAssignedToUsers: false, - canBeAssignedToApiKeys: false, - objectPermissions: [ - { - objectUniversalIdentifier: - STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.calendarEvent.universalIdentifier, - canReadObjectRecords: true, - canUpdateObjectRecords: false, - canSoftDeleteObjectRecords: false, - canDestroyObjectRecords: false, - }, - { - objectUniversalIdentifier: - STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.calendarEventParticipant - .universalIdentifier, - canReadObjectRecords: true, - canUpdateObjectRecords: false, - canSoftDeleteObjectRecords: false, - canDestroyObjectRecords: false, - }, - { - objectUniversalIdentifier: - STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.callRecording.universalIdentifier, - canReadObjectRecords: true, - canUpdateObjectRecords: true, - canSoftDeleteObjectRecords: false, - canDestroyObjectRecords: false, - }, - { - objectUniversalIdentifier: - STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.person.universalIdentifier, - canReadObjectRecords: true, - canUpdateObjectRecords: false, - canSoftDeleteObjectRecords: false, - canDestroyObjectRecords: false, - }, - { - objectUniversalIdentifier: - STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.workspaceMember - .universalIdentifier, - canReadObjectRecords: true, - canUpdateObjectRecords: false, - canSoftDeleteObjectRecords: false, - canDestroyObjectRecords: false, - }, - ], - fieldPermissions: [], - permissionFlagUniversalIdentifiers: [SystemPermissionFlag.UPLOAD_FILE], -}); diff --git a/packages/twenty-apps/public/twenty-meeting-bot/src/fields/meeting-bot-failure-reason-on-call-recording.field.ts b/packages/twenty-apps/public/twenty-meeting-bot/src/fields/meeting-bot-failure-reason-on-call-recording.field.ts deleted file mode 100644 index af87ce4b8728a..0000000000000 --- a/packages/twenty-apps/public/twenty-meeting-bot/src/fields/meeting-bot-failure-reason-on-call-recording.field.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { - defineField, - FieldType, - STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS, -} from 'twenty-sdk/define'; - -import { MEETING_BOT_FAILURE_REASON_ON_CALL_RECORDING_FIELD_UNIVERSAL_IDENTIFIER } from 'src/constants/meeting-bot-failure-reason-on-call-recording-field-universal-identifier'; - -export default defineField({ - universalIdentifier: - MEETING_BOT_FAILURE_REASON_ON_CALL_RECORDING_FIELD_UNIVERSAL_IDENTIFIER, - objectUniversalIdentifier: - STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.callRecording.universalIdentifier, - type: FieldType.TEXT, - name: 'meetingBotFailureReason', - label: 'Meeting Bot Failure Reason', - description: - 'Provider-specific reason the meeting bot could not produce a recording.', - icon: 'IconAlertTriangle', - isNullable: true, - isUIEditable: false, -}); diff --git a/packages/twenty-apps/public/twenty-meeting-bot/src/fields/meeting-bot-preference-on-calendar-event.field.ts b/packages/twenty-apps/public/twenty-meeting-bot/src/fields/meeting-bot-preference-on-calendar-event.field.ts deleted file mode 100644 index 078c91a327fb5..0000000000000 --- a/packages/twenty-apps/public/twenty-meeting-bot/src/fields/meeting-bot-preference-on-calendar-event.field.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { - defineField, - FieldType, - STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS, -} from 'twenty-sdk/define'; - -import { MeetingBotPreference } from 'src/constants/meeting-bot-preference'; -import { MEETING_BOT_PREFERENCE_OFF_OPTION_ID } from 'src/constants/meeting-bot-preference-off-option-id'; -import { MEETING_BOT_PREFERENCE_ON_OPTION_ID } from 'src/constants/meeting-bot-preference-on-option-id'; -import { MEETING_BOT_PREFERENCE_ON_CALENDAR_EVENT_FIELD_UNIVERSAL_IDENTIFIER } from 'src/constants/meeting-bot-preference-on-calendar-event-field-universal-identifier'; - -export default defineField({ - universalIdentifier: - MEETING_BOT_PREFERENCE_ON_CALENDAR_EVENT_FIELD_UNIVERSAL_IDENTIFIER, - objectUniversalIdentifier: - STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.calendarEvent.universalIdentifier, - type: FieldType.SELECT, - name: 'meetingBotPreference', - label: 'Recording Bot', - description: - 'Meeting bot recording is on by default when the app is installed. Turn it off for this event when needed.', - icon: 'IconRobot', - isNullable: false, - defaultValue: `'${MeetingBotPreference.ON}'`, - options: [ - { - id: MEETING_BOT_PREFERENCE_ON_OPTION_ID, - value: MeetingBotPreference.ON, - label: 'On', - position: 0, - color: 'green', - }, - { - id: MEETING_BOT_PREFERENCE_OFF_OPTION_ID, - value: MeetingBotPreference.OFF, - label: 'Off', - position: 1, - color: 'red', - }, - ], -}); diff --git a/packages/twenty-apps/public/twenty-meeting-bot/src/front-components/calendar-event-recording.front-component.tsx b/packages/twenty-apps/public/twenty-meeting-bot/src/front-components/calendar-event-recording.front-component.tsx deleted file mode 100644 index 76eae30dc5176..0000000000000 --- a/packages/twenty-apps/public/twenty-meeting-bot/src/front-components/calendar-event-recording.front-component.tsx +++ /dev/null @@ -1,13 +0,0 @@ -import { defineFrontComponent } from 'twenty-sdk/define'; - -import { CALENDAR_EVENT_RECORDING_FRONT_COMPONENT_UNIVERSAL_IDENTIFIER } from 'src/constants/calendar-event-recording-front-component-universal-identifier'; -import { CalendarEventRecording } from 'src/front-components/components/CalendarEventRecording'; - -export default defineFrontComponent({ - universalIdentifier: - CALENDAR_EVENT_RECORDING_FRONT_COMPONENT_UNIVERSAL_IDENTIFIER, - name: 'calendar-event-recording', - description: - 'Read-only recording viewer with synced transcript for the calendar event record page.', - component: CalendarEventRecording, -}); diff --git a/packages/twenty-apps/public/twenty-meeting-bot/src/front-components/components/CalendarEventRecording.tsx b/packages/twenty-apps/public/twenty-meeting-bot/src/front-components/components/CalendarEventRecording.tsx deleted file mode 100644 index ce05393821805..0000000000000 --- a/packages/twenty-apps/public/twenty-meeting-bot/src/front-components/components/CalendarEventRecording.tsx +++ /dev/null @@ -1,39 +0,0 @@ -import styled from '@emotion/styled'; -import { isUndefined } from '@sniptt/guards'; -import { useSelectedRecordIds } from 'twenty-sdk/front-component'; - -import { CalendarEventRecordingContent } from 'src/front-components/components/CalendarEventRecordingContent'; -import { recordingThemeCssVariables } from 'src/front-components/constants/recording-theme-css-variables'; - -const StyledCenteredState = styled.div` - align-items: center; - box-sizing: border-box; - color: ${recordingThemeCssVariables.font.colorTertiary}; - display: flex; - font-family: ${recordingThemeCssVariables.font.family}; - font-size: ${recordingThemeCssVariables.font.sizeSm}; - height: 100%; - justify-content: center; - padding: ${recordingThemeCssVariables.spacing[4]}; -`; - -export const CalendarEventRecording = () => { - const selectedRecordIds = useSelectedRecordIds(); - const calendarEventId = - selectedRecordIds.length === 1 ? selectedRecordIds[0] : undefined; - - if (isUndefined(calendarEventId)) { - return ( - - Open a calendar event to see its recording. - - ); - } - - return ( - - ); -}; diff --git a/packages/twenty-apps/public/twenty-meeting-bot/src/front-components/components/CalendarEventRecordingBody.tsx b/packages/twenty-apps/public/twenty-meeting-bot/src/front-components/components/CalendarEventRecordingBody.tsx deleted file mode 100644 index 7847f43409573..0000000000000 --- a/packages/twenty-apps/public/twenty-meeting-bot/src/front-components/components/CalendarEventRecordingBody.tsx +++ /dev/null @@ -1,96 +0,0 @@ -import styled from '@emotion/styled'; -import { isUndefined } from '@sniptt/guards'; - -import { RecordingTranscript } from 'src/front-components/components/RecordingTranscript'; -import { RecordingVideoPlayer } from 'src/front-components/components/RecordingVideoPlayer'; -import { TranscriptErrorBox } from 'src/front-components/components/TranscriptErrorBox'; -import { recordingThemeCssVariables } from 'src/front-components/constants/recording-theme-css-variables'; -import { type CalendarEventRecordingParticipant } from 'src/front-components/types/calendar-event-recording-participant.type'; - -const StyledCenteredState = styled.div` - align-items: center; - box-sizing: border-box; - color: ${recordingThemeCssVariables.font.colorTertiary}; - display: flex; - font-family: ${recordingThemeCssVariables.font.family}; - font-size: ${recordingThemeCssVariables.font.sizeSm}; - justify-content: center; - min-height: 240px; - padding: ${recordingThemeCssVariables.spacing[4]}; -`; - -const StyledRecordingContainer = styled.div<{ - $hasVideo?: boolean; -}>` - display: grid; - gap: ${recordingThemeCssVariables.spacing[2]}; - grid-template-rows: ${({ $hasVideo }) => - $hasVideo ? 'auto minmax(0, 1fr)' : 'minmax(0, 1fr)'}; - min-height: 0; -`; - -type CalendarEventRecordingBodyProps = { - transcript: unknown; - videoFileUrl: string | undefined; - isCalendarEventRecordingQueryLoading: boolean; - errorMessage: string | undefined; - currentTimeSeconds: number; - calendarEventParticipants: CalendarEventRecordingParticipant[]; - onVideoTimeUpdate: (videoCurrentTimeSeconds: number) => void; -}; - -export const CalendarEventRecordingBody = ({ - transcript, - videoFileUrl, - isCalendarEventRecordingQueryLoading, - errorMessage, - currentTimeSeconds, - calendarEventParticipants, - onVideoTimeUpdate, -}: CalendarEventRecordingBodyProps) => { - const hasVideo = !isUndefined(videoFileUrl); - - if (!isUndefined(errorMessage)) { - return ( - - ); - } - - if (isCalendarEventRecordingQueryLoading) { - return ( - - - - ); - } - - if (isUndefined(transcript) && !hasVideo) { - return ( - - No recording for this calendar event yet. - - ); - } - - return ( - - {hasVideo && ( - - )} - - - ); -}; diff --git a/packages/twenty-apps/public/twenty-meeting-bot/src/front-components/components/CalendarEventRecordingContent.tsx b/packages/twenty-apps/public/twenty-meeting-bot/src/front-components/components/CalendarEventRecordingContent.tsx deleted file mode 100644 index 3b1e5f07ec60f..0000000000000 --- a/packages/twenty-apps/public/twenty-meeting-bot/src/front-components/components/CalendarEventRecordingContent.tsx +++ /dev/null @@ -1,111 +0,0 @@ -import styled from '@emotion/styled'; -import { useCallback, useState } from 'react'; - -import { CalendarEventRecordingBody } from 'src/front-components/components/CalendarEventRecordingBody'; -import { recordingThemeCssVariables } from 'src/front-components/constants/recording-theme-css-variables'; -import { useCalendarEventParticipants } from 'src/front-components/hooks/use-calendar-event-participants'; -import { useCalendarEventRecording } from 'src/front-components/hooks/use-calendar-event-recording'; - -const TRANSCRIPT_TIME_UPDATE_INTERVAL_SECONDS = 0.25; - -const StyledRecordingShell = styled.div` - background: ${recordingThemeCssVariables.background.primary}; - border: 1px solid transparent; - border-bottom: 1px solid transparent; - border-radius: ${recordingThemeCssVariables.border.radiusMd}; - box-sizing: border-box; - font-family: ${recordingThemeCssVariables.font.family}; - padding: ${recordingThemeCssVariables.spacing[4]}; - position: relative; - width: 100%; -`; - -const StyledRecordingHeader = styled.div` - align-items: center; - box-sizing: border-box; - display: flex; - height: ${recordingThemeCssVariables.spacing[6]}; -`; - -const StyledRecordingTitle = styled.h2` - color: ${recordingThemeCssVariables.font.colorPrimary}; - flex: 1; - font-size: ${recordingThemeCssVariables.font.sizeMd}; - font-weight: ${recordingThemeCssVariables.font.weightMedium}; - margin: 0; - overflow: hidden; - padding-inline: ${recordingThemeCssVariables.spacing[1]}; - user-select: none; -`; - -const StyledRecordingBody = styled.div` - box-sizing: border-box; - margin-top: ${recordingThemeCssVariables.spacing[2]}; -`; - -const StyledRecordingContentFrame = styled.div` - background-color: ${recordingThemeCssVariables.background.secondary}; - border: 1px solid ${recordingThemeCssVariables.border.colorMedium}; - border-radius: ${recordingThemeCssVariables.border.radiusMd}; - box-sizing: border-box; - padding: ${recordingThemeCssVariables.spacing[2]}; -`; - -type CalendarEventRecordingContentProps = { - calendarEventId: string; -}; - -export const CalendarEventRecordingContent = ({ - calendarEventId, -}: CalendarEventRecordingContentProps) => { - const [currentTimeSeconds, setCurrentTimeSeconds] = useState(0); - const updateCurrentTimeSeconds = useCallback( - (videoCurrentTimeSeconds: number) => { - const nextCurrentTimeSeconds = - Math.floor( - videoCurrentTimeSeconds / TRANSCRIPT_TIME_UPDATE_INTERVAL_SECONDS, - ) * TRANSCRIPT_TIME_UPDATE_INTERVAL_SECONDS; - - setCurrentTimeSeconds((previousCurrentTimeSeconds) => - previousCurrentTimeSeconds === nextCurrentTimeSeconds - ? previousCurrentTimeSeconds - : nextCurrentTimeSeconds, - ); - }, - [], - ); - - const { - transcript, - videoFile, - isCalendarEventRecordingQueryLoading, - errorMessage, - } = useCalendarEventRecording(calendarEventId); - const { calendarEventParticipants } = - useCalendarEventParticipants(calendarEventId); - - const videoFileUrl = videoFile?.url ?? undefined; - - return ( - - - Recording and Transcript - - - - - - - - ); -}; diff --git a/packages/twenty-apps/public/twenty-meeting-bot/src/front-components/components/RecordingTranscript.tsx b/packages/twenty-apps/public/twenty-meeting-bot/src/front-components/components/RecordingTranscript.tsx deleted file mode 100644 index 5666e2f5e0ba4..0000000000000 --- a/packages/twenty-apps/public/twenty-meeting-bot/src/front-components/components/RecordingTranscript.tsx +++ /dev/null @@ -1,92 +0,0 @@ -import styled from '@emotion/styled'; -import { isUndefined } from '@sniptt/guards'; -import { useMemo } from 'react'; - -import { TranscriptEntryList } from 'src/front-components/components/TranscriptEntryList'; -import { TranscriptErrorBox } from 'src/front-components/components/TranscriptErrorBox'; -import { recordingThemeCssVariables } from 'src/front-components/constants/recording-theme-css-variables'; -import { type CalendarEventRecordingParticipant } from 'src/front-components/types/calendar-event-recording-participant.type'; -import { parseTranscriptEntries } from 'src/front-components/utils/parse-transcript-entries.util'; -import { parseTranscriptMarker } from 'src/logic-functions/domain/parse-transcript-marker.util'; -import { isNonEmptyString } from 'src/logic-functions/utils/is-non-empty-string.util'; - -const StyledTranscriptCenteredState = styled.div` - align-items: center; - color: ${recordingThemeCssVariables.font.colorTertiary}; - display: flex; - flex: 1; - font-size: ${recordingThemeCssVariables.font.sizeSm}; - justify-content: center; -`; - -type RecordingTranscriptProps = { - transcript: unknown; - currentTimeSeconds: number; - calendarEventParticipants: CalendarEventRecordingParticipant[]; -}; - -export const RecordingTranscript = ({ - transcript, - currentTimeSeconds, - calendarEventParticipants, -}: RecordingTranscriptProps) => { - const marker = useMemo(() => parseTranscriptMarker(transcript), [transcript]); - const entries = useMemo( - () => parseTranscriptEntries(transcript), - [transcript], - ); - - if (isUndefined(transcript)) { - return ( - - No transcript for this calendar event yet. - - ); - } - - if (marker?.status === 'PENDING') { - return ( - - The transcript is being generated. Check back in a few minutes. - - ); - } - - if (marker?.status === 'FAILED') { - return ( - - ); - } - - if (isUndefined(entries)) { - return ( - - ); - } - - if (entries.length === 0) { - return ( - - The transcript is empty. - - ); - } - - return ( - - ); -}; diff --git a/packages/twenty-apps/public/twenty-meeting-bot/src/front-components/components/RecordingVideoPlayer.tsx b/packages/twenty-apps/public/twenty-meeting-bot/src/front-components/components/RecordingVideoPlayer.tsx deleted file mode 100644 index 40a60ab04a009..0000000000000 --- a/packages/twenty-apps/public/twenty-meeting-bot/src/front-components/components/RecordingVideoPlayer.tsx +++ /dev/null @@ -1,52 +0,0 @@ -import styled from '@emotion/styled'; -import { memo, type SyntheticEvent } from 'react'; - -import { recordingThemeCssVariables } from 'src/front-components/constants/recording-theme-css-variables'; - -const DEFAULT_VIDEO_ASPECT_RATIO = '16 / 9'; - -const StyledVideoViewport = styled.div` - aspect-ratio: ${DEFAULT_VIDEO_ASPECT_RATIO}; - background: ${recordingThemeCssVariables.background.primary}; - border-radius: ${recordingThemeCssVariables.border.radiusSm}; - overflow: hidden; - width: 100%; -`; - -const StyledVideo = styled.video` - accent-color: ${recordingThemeCssVariables.accent.primary}; - background: ${recordingThemeCssVariables.background.primary}; - color-scheme: light dark; - display: block; - height: 100%; - object-fit: contain; - width: 100%; -`; - -type RecordingVideoPlayerProps = { - src: string | undefined; - onTimeUpdate: (currentTimeSeconds: number) => void; -}; - -const RecordingVideoPlayerComponent = ({ - src, - onTimeUpdate, -}: RecordingVideoPlayerProps) => { - const handleTimeUpdate = (event: SyntheticEvent) => { - onTimeUpdate(event.currentTarget.currentTime); - }; - - return ( - - - - ); -}; - -export const RecordingVideoPlayer = memo(RecordingVideoPlayerComponent); diff --git a/packages/twenty-apps/public/twenty-meeting-bot/src/front-components/components/TranscriptEntryList.tsx b/packages/twenty-apps/public/twenty-meeting-bot/src/front-components/components/TranscriptEntryList.tsx deleted file mode 100644 index 43f4b7a479372..0000000000000 --- a/packages/twenty-apps/public/twenty-meeting-bot/src/front-components/components/TranscriptEntryList.tsx +++ /dev/null @@ -1,61 +0,0 @@ -import styled from '@emotion/styled'; -import { useMemo } from 'react'; - -import { TranscriptEntryListItem } from 'src/front-components/components/TranscriptEntryListItem'; -import { recordingThemeCssVariables } from 'src/front-components/constants/recording-theme-css-variables'; -import { type CalendarEventRecordingParticipant } from 'src/front-components/types/calendar-event-recording-participant.type'; -import { type TranscriptEntry } from 'src/front-components/types/transcript-entry.type'; -import { buildCalendarEventParticipantBySpeakerName } from 'src/front-components/utils/build-calendar-event-participant-by-speaker-name.util'; -import { findActiveTranscriptEntryIndex } from 'src/front-components/utils/find-active-transcript-entry-index.util'; -import { getCalendarEventParticipantForSpeakerName } from 'src/front-components/utils/get-calendar-event-participant-for-speaker-name.util'; - -const StyledTranscriptContainer = styled.div` - display: flex; - flex: 1; - flex-direction: column; - gap: ${recordingThemeCssVariables.spacing[2]}; - min-height: 0; -`; - -type TranscriptEntryListProps = { - entries: TranscriptEntry[]; - currentTimeSeconds: number; - calendarEventParticipants: CalendarEventRecordingParticipant[]; -}; - -export const TranscriptEntryList = ({ - entries, - currentTimeSeconds, - calendarEventParticipants, -}: TranscriptEntryListProps) => { - const activeEntryIndex = findActiveTranscriptEntryIndex( - entries, - currentTimeSeconds, - ); - const calendarEventParticipantBySpeakerName = useMemo( - () => buildCalendarEventParticipantBySpeakerName(calendarEventParticipants), - [calendarEventParticipants], - ); - - return ( - - {entries.map((entry, entryIndex) => { - const calendarEventParticipant = - getCalendarEventParticipantForSpeakerName({ - speakerName: entry.speakerName, - calendarEventParticipantBySpeakerName, - }); - - return ( - - ); - })} - - ); -}; diff --git a/packages/twenty-apps/public/twenty-meeting-bot/src/front-components/components/TranscriptEntryListItem.tsx b/packages/twenty-apps/public/twenty-meeting-bot/src/front-components/components/TranscriptEntryListItem.tsx deleted file mode 100644 index 044489c48b870..0000000000000 --- a/packages/twenty-apps/public/twenty-meeting-bot/src/front-components/components/TranscriptEntryListItem.tsx +++ /dev/null @@ -1,115 +0,0 @@ -import styled from '@emotion/styled'; -import { isUndefined } from '@sniptt/guards'; - -import { TranscriptSpeakerChip } from 'src/front-components/components/TranscriptSpeakerChip'; -import { recordingThemeCssVariables } from 'src/front-components/constants/recording-theme-css-variables'; -import { type CalendarEventRecordingParticipant } from 'src/front-components/types/calendar-event-recording-participant.type'; -import { - type TranscriptEntry, - type TranscriptWord, -} from 'src/front-components/types/transcript-entry.type'; -import { formatTranscriptTimestamp } from 'src/front-components/utils/format-transcript-timestamp.util'; - -const StyledEntry = styled.div<{ $isActive: boolean }>` - align-items: flex-start; - background: ${({ $isActive }) => - $isActive - ? recordingThemeCssVariables.background.transparentBlue - : 'transparent'}; - border-radius: ${recordingThemeCssVariables.border.radiusSm}; - box-sizing: border-box; - display: flex; - flex-direction: column; - gap: ${recordingThemeCssVariables.spacing[2]}; - justify-content: center; - padding: ${recordingThemeCssVariables.spacing[2]}; - width: 100%; -`; - -const StyledEntryHeader = styled.div` - align-items: center; - align-self: stretch; - display: flex; - gap: ${recordingThemeCssVariables.spacing[2]}; - min-height: ${recordingThemeCssVariables.spacing[6]}; - min-width: 0; -`; - -const StyledTimestamp = styled.span` - color: ${recordingThemeCssVariables.font.colorTertiary}; - font-size: ${recordingThemeCssVariables.font.sizeXs}; - line-height: 1.4; -`; - -const StyledEntryText = styled.p` - align-self: stretch; - color: ${recordingThemeCssVariables.font.colorSecondary}; - font-size: ${recordingThemeCssVariables.font.sizeSm}; - line-height: 1.4; - margin: 0; -`; - -const StyledWord = styled.span<{ $isSpoken: boolean }>` - color: ${({ $isSpoken }) => - $isSpoken - ? recordingThemeCssVariables.font.colorPrimary - : recordingThemeCssVariables.font.colorSecondary}; - line-height: 1.4; - transition: color 0.15s ease; -`; - -type TranscriptEntryListItemProps = { - entry: TranscriptEntry; - isActive: boolean; - currentTimeSeconds: number; - calendarEventParticipant: CalendarEventRecordingParticipant | undefined; -}; - -export const TranscriptEntryListItem = ({ - entry, - isActive, - currentTimeSeconds, - calendarEventParticipant, -}: TranscriptEntryListItemProps) => { - const speakerDisplayName = - calendarEventParticipant?.displayName ?? entry.speakerName; - - return ( - - - - {!isUndefined(entry.startSeconds) && ( - - {formatTranscriptTimestamp(entry.startSeconds)} - - )} - - - {entry.words.map((word, wordIndex) => ( - - {wordIndex > 0 ? ' ' : ''} - {word.text} - - ))} - - - ); -}; - -const isWordSpoken = ({ - word, - currentTimeSeconds, -}: { - word: TranscriptWord; - currentTimeSeconds: number; -}): boolean => - !isUndefined(word.startSeconds) && currentTimeSeconds >= word.startSeconds; diff --git a/packages/twenty-apps/public/twenty-meeting-bot/src/front-components/components/TranscriptErrorBox.tsx b/packages/twenty-apps/public/twenty-meeting-bot/src/front-components/components/TranscriptErrorBox.tsx deleted file mode 100644 index 2808bd3f97be1..0000000000000 --- a/packages/twenty-apps/public/twenty-meeting-bot/src/front-components/components/TranscriptErrorBox.tsx +++ /dev/null @@ -1,48 +0,0 @@ -import styled from '@emotion/styled'; - -import { recordingThemeCssVariables } from 'src/front-components/constants/recording-theme-css-variables'; - -const StyledStateContainer = styled.div` - box-sizing: border-box; - font-family: ${recordingThemeCssVariables.font.family}; - height: 100%; - padding: ${recordingThemeCssVariables.spacing[4]}; -`; - -const StyledErrorBox = styled.div` - background: ${recordingThemeCssVariables.background.transparentDanger}; - border: 1px solid ${recordingThemeCssVariables.border.colorDanger}; - border-radius: ${recordingThemeCssVariables.border.radiusMd}; - display: flex; - flex-direction: column; - gap: ${recordingThemeCssVariables.spacing[1]}; - padding: ${recordingThemeCssVariables.spacing[3]}; -`; - -const StyledErrorTitle = styled.span` - color: ${recordingThemeCssVariables.font.colorDanger}; - font-size: ${recordingThemeCssVariables.font.sizeSm}; - font-weight: ${recordingThemeCssVariables.font.weightMedium}; -`; - -const StyledErrorDescription = styled.span` - color: ${recordingThemeCssVariables.font.colorSecondary}; - font-size: ${recordingThemeCssVariables.font.sizeSm}; -`; - -type TranscriptErrorBoxProps = { - title: string; - description: string; -}; - -export const TranscriptErrorBox = ({ - title, - description, -}: TranscriptErrorBoxProps) => ( - - - {title} - {description} - - -); diff --git a/packages/twenty-apps/public/twenty-meeting-bot/src/front-components/components/TranscriptSpeakerAvatar.tsx b/packages/twenty-apps/public/twenty-meeting-bot/src/front-components/components/TranscriptSpeakerAvatar.tsx deleted file mode 100644 index c2695ab2c3e96..0000000000000 --- a/packages/twenty-apps/public/twenty-meeting-bot/src/front-components/components/TranscriptSpeakerAvatar.tsx +++ /dev/null @@ -1,141 +0,0 @@ -// Duplicates minimal twenty-ui Avatar logic for this app. -// Remove once twenty-ui can be imported safely in front components. -import styled from '@emotion/styled'; -import { useState } from 'react'; - -import { recordingThemeCssVariables } from 'src/front-components/constants/recording-theme-css-variables'; -import { isNonEmptyString } from 'src/logic-functions/utils/is-non-empty-string.util'; - -const AVATAR_COLOR_NAMES = [ - 'red', - 'ruby', - 'crimson', - 'tomato', - 'orange', - 'amber', - 'yellow', - 'lime', - 'grass', - 'green', - 'jade', - 'mint', - 'turquoise', - 'cyan', - 'sky', - 'blue', - 'iris', - 'violet', - 'purple', - 'plum', - 'pink', - 'bronze', - 'gold', - 'brown', - 'gray', -] as const; - -const StyledAvatar = styled.div<{ - $backgroundColor: string; - $color: string; -}>` - align-items: center; - background: ${({ $backgroundColor }) => $backgroundColor}; - border-radius: 50px; - box-sizing: border-box; - color: ${({ $color }) => $color}; - display: flex; - flex-shrink: 0; - font-size: ${recordingThemeCssVariables.font.sizeXs}; - font-weight: ${recordingThemeCssVariables.font.weightMedium}; - height: 16px; - justify-content: center; - line-height: 15px; - overflow: hidden; - width: 16px; -`; - -const StyledAvatarImage = styled.img` - height: 100%; - object-fit: cover; - width: 100%; -`; - -type TranscriptSpeakerAvatarProps = { - speakerName: string; - avatarUrl: string | undefined; - placeholderColorSeed: string; -}; - -const getSpeakerInitial = (speakerName: string) => - speakerName.trim().charAt(0).toUpperCase() || '-'; - -export const TranscriptSpeakerAvatar = ({ - speakerName, - avatarUrl, - placeholderColorSeed, -}: TranscriptSpeakerAvatarProps) => { - const [erroredAvatarUrl, setErroredAvatarUrl] = useState< - string | undefined - >(undefined); - - const shouldShowAvatarImage = - isNonEmptyString(avatarUrl) && erroredAvatarUrl !== avatarUrl; - - const handleAvatarImageError = () => { - if (isNonEmptyString(avatarUrl)) { - setErroredAvatarUrl(avatarUrl); - } - }; - - const avatarPlaceholderColor = getAvatarPlaceholderColor({ - placeholderColorSeed, - variant: 12, - }); - const avatarPlaceholderBackgroundColor = getAvatarPlaceholderColor({ - placeholderColorSeed, - variant: 4, - }); - - return ( - - ); -}; - -const getAvatarPlaceholderColor = ({ - placeholderColorSeed, - variant, -}: { - placeholderColorSeed: string; - variant: 4 | 12; -}): string => { - const avatarColorName = - AVATAR_COLOR_NAMES[ - Math.abs(hashString(placeholderColorSeed)) % AVATAR_COLOR_NAMES.length - ]; - - return `var(--t-color-${avatarColorName}${variant})`; -}; - -const hashString = (value: string): number => { - let hash = 0; - - for (let valueIndex = 0; valueIndex < value.length; valueIndex++) { - hash = value.charCodeAt(valueIndex) + ((hash << 5) - hash); - } - - return hash; -}; diff --git a/packages/twenty-apps/public/twenty-meeting-bot/src/front-components/components/TranscriptSpeakerChip.tsx b/packages/twenty-apps/public/twenty-meeting-bot/src/front-components/components/TranscriptSpeakerChip.tsx deleted file mode 100644 index d7863eb42a405..0000000000000 --- a/packages/twenty-apps/public/twenty-meeting-bot/src/front-components/components/TranscriptSpeakerChip.tsx +++ /dev/null @@ -1,51 +0,0 @@ -// Duplicates minimal twenty-ui Chip logic for this app. -// Remove once twenty-ui can be imported safely in front components. -import styled from '@emotion/styled'; - -import { TranscriptSpeakerAvatar } from 'src/front-components/components/TranscriptSpeakerAvatar'; -import { recordingThemeCssVariables } from 'src/front-components/constants/recording-theme-css-variables'; - -const StyledSpeakerChip = styled.span` - align-items: center; - border-radius: ${recordingThemeCssVariables.border.radiusSm}; - color: ${recordingThemeCssVariables.font.colorPrimary}; - display: inline-flex; - font-size: ${recordingThemeCssVariables.font.sizeSm}; - font-weight: ${recordingThemeCssVariables.font.weightMedium}; - gap: ${recordingThemeCssVariables.spacing[1]}; - line-height: 1.4; - max-width: 100%; - min-width: 0; - text-decoration: none; - white-space: nowrap; -`; - -const StyledSpeakerName = styled.span` - flex-shrink: 1; - min-width: 0; - overflow: hidden; - text-overflow: ellipsis; -`; - -type TranscriptSpeakerChipProps = { - speakerName: string; - avatarUrl: string | undefined; - placeholderColorSeed: string; -}; - -export const TranscriptSpeakerChip = ({ - speakerName, - avatarUrl, - placeholderColorSeed, -}: TranscriptSpeakerChipProps) => { - return ( - - - {speakerName} - - ); -}; diff --git a/packages/twenty-apps/public/twenty-meeting-bot/src/front-components/constants/recording-theme-css-variables.ts b/packages/twenty-apps/public/twenty-meeting-bot/src/front-components/constants/recording-theme-css-variables.ts deleted file mode 100644 index a8cce2347fcb7..0000000000000 --- a/packages/twenty-apps/public/twenty-meeting-bot/src/front-components/constants/recording-theme-css-variables.ts +++ /dev/null @@ -1,40 +0,0 @@ -// Avoid the SDK UI entrypoint until its bundle is safe for the browser runtime. -export const recordingThemeCssVariables = { - accent: { - primary: 'var(--t-accent-accent9)', - }, - background: { - primary: 'var(--t-background-primary)', - secondary: 'var(--t-background-secondary)', - transparentBlue: 'var(--t-background-transparent-blue)', - transparentDanger: 'var(--t-background-transparent-danger)', - }, - border: { - colorDanger: 'var(--t-border-color-danger)', - colorLight: 'var(--t-border-color-light)', - colorMedium: 'var(--t-border-color-medium)', - radiusMd: 'var(--t-border-radius-md)', - radiusSm: 'var(--t-border-radius-sm)', - }, - boxShadow: { - light: 'var(--t-box-shadow-light)', - }, - font: { - colorDanger: 'var(--t-font-color-danger)', - colorPrimary: 'var(--t-font-color-primary)', - colorSecondary: 'var(--t-font-color-secondary)', - colorTertiary: 'var(--t-font-color-tertiary)', - family: 'var(--t-font-family)', - sizeMd: 'var(--t-font-size-md)', - sizeSm: 'var(--t-font-size-sm)', - sizeXs: 'var(--t-font-size-xs)', - weightMedium: 'var(--t-font-weight-medium)', - }, - spacing: { - 1: 'var(--t-spacing-1)', - 2: 'var(--t-spacing-2)', - 3: 'var(--t-spacing-3)', - 4: 'var(--t-spacing-4)', - 6: 'var(--t-spacing-6)', - }, -} as const; diff --git a/packages/twenty-apps/public/twenty-meeting-bot/src/front-components/hooks/use-calendar-event-participants.ts b/packages/twenty-apps/public/twenty-meeting-bot/src/front-components/hooks/use-calendar-event-participants.ts deleted file mode 100644 index a1eb0ef14a04b..0000000000000 --- a/packages/twenty-apps/public/twenty-meeting-bot/src/front-components/hooks/use-calendar-event-participants.ts +++ /dev/null @@ -1,174 +0,0 @@ -import { useEffect, useState } from 'react'; -import { CoreApiClient } from 'twenty-client-sdk/core'; - -import { type CalendarEventRecordingParticipant } from 'src/front-components/types/calendar-event-recording-participant.type'; -import { getAbsoluteAvatarUrl } from 'src/front-components/utils/get-absolute-avatar-url.util'; -import { isNonEmptyString } from 'src/logic-functions/utils/is-non-empty-string.util'; - -const CALENDAR_EVENT_PARTICIPANT_LOOKUP_LIMIT = 100; - -type CalendarEventParticipantName = { - firstName?: string | null; - lastName?: string | null; -}; - -type CalendarEventParticipantRelatedRecord = { - id?: string | null; - avatarUrl?: string | null; - name?: CalendarEventParticipantName | null; -}; - -type CalendarEventParticipantNode = { - id: string; - displayName?: string | null; - handle?: string | null; - personId?: string | null; - workspaceMemberId?: string | null; - person?: CalendarEventParticipantRelatedRecord | null; - workspaceMember?: CalendarEventParticipantRelatedRecord | null; -}; - -type CalendarEventParticipantEdge = { - node: CalendarEventParticipantNode; -}; - -type UseCalendarEventParticipantsReturn = { - calendarEventParticipants: CalendarEventRecordingParticipant[]; -}; - -export const useCalendarEventParticipants = ( - calendarEventId: string | undefined, -): UseCalendarEventParticipantsReturn => { - const [calendarEventParticipants, setCalendarEventParticipants] = useState< - CalendarEventRecordingParticipant[] - >([]); - - useEffect(() => { - if (!isNonEmptyString(calendarEventId)) { - setCalendarEventParticipants([]); - return; - } - - let cancelled = false; - - const fetchCalendarEventParticipants = async () => { - try { - const client = new CoreApiClient(); - const queryResult = await client.query({ - calendarEventParticipants: { - __args: { - filter: { calendarEventId: { eq: calendarEventId } }, - first: CALENDAR_EVENT_PARTICIPANT_LOOKUP_LIMIT, - }, - edges: { - node: { - id: true, - displayName: true, - handle: true, - personId: true, - workspaceMemberId: true, - person: { - id: true, - avatarUrl: true, - name: { - firstName: true, - lastName: true, - }, - }, - workspaceMember: { - id: true, - avatarUrl: true, - name: { - firstName: true, - lastName: true, - }, - }, - }, - }, - }, - }); - - if (cancelled) { - return; - } - - const calendarEventParticipantEdges = (queryResult - .calendarEventParticipants?.edges ?? - []) as CalendarEventParticipantEdge[]; - - setCalendarEventParticipants( - calendarEventParticipantEdges.map((calendarEventParticipantEdge) => - mapCalendarEventParticipantNode( - calendarEventParticipantEdge.node, - ), - ), - ); - } catch { - if (cancelled) { - return; - } - - setCalendarEventParticipants([]); - } - }; - - fetchCalendarEventParticipants(); - - return () => { - cancelled = true; - }; - }, [calendarEventId]); - - return { calendarEventParticipants }; -}; - -const mapCalendarEventParticipantNode = ( - calendarEventParticipantNode: CalendarEventParticipantNode, -): CalendarEventRecordingParticipant => { - const personName = readFullName(calendarEventParticipantNode.person?.name); - const workspaceMemberName = readFullName( - calendarEventParticipantNode.workspaceMember?.name, - ); - const calendarDisplayName = readOptionalString( - calendarEventParticipantNode.displayName, - ); - const handle = readOptionalString(calendarEventParticipantNode.handle); - - return { - id: calendarEventParticipantNode.id, - avatarUrl: getAbsoluteAvatarUrl( - calendarEventParticipantNode.person?.avatarUrl ?? - calendarEventParticipantNode.workspaceMember?.avatarUrl, - ), - displayName: - personName ?? workspaceMemberName ?? calendarDisplayName ?? handle, - nameCandidates: [ - calendarDisplayName, - personName, - workspaceMemberName, - handle, - ].filter((nameCandidate): nameCandidate is string => - isNonEmptyString(nameCandidate), - ), - placeholderColorSeed: - calendarEventParticipantNode.workspaceMemberId ?? - calendarEventParticipantNode.personId ?? - calendarEventParticipantNode.id, - }; -}; - -const readFullName = ( - name: CalendarEventParticipantName | null | undefined, -): string | undefined => { - const firstName = readOptionalString(name?.firstName); - const lastName = readOptionalString(name?.lastName); - const fullName = [firstName, lastName] - .filter((namePart): namePart is string => isNonEmptyString(namePart)) - .join(' '); - - return isNonEmptyString(fullName) ? fullName : undefined; -}; - -const readOptionalString = ( - value: string | null | undefined, -): string | undefined => (isNonEmptyString(value) ? value.trim() : undefined); diff --git a/packages/twenty-apps/public/twenty-meeting-bot/src/front-components/hooks/use-calendar-event-recording.ts b/packages/twenty-apps/public/twenty-meeting-bot/src/front-components/hooks/use-calendar-event-recording.ts deleted file mode 100644 index d1bb8803e49cb..0000000000000 --- a/packages/twenty-apps/public/twenty-meeting-bot/src/front-components/hooks/use-calendar-event-recording.ts +++ /dev/null @@ -1,156 +0,0 @@ -import { isUndefined } from '@sniptt/guards'; -import { useEffect, useState } from 'react'; -import { CoreApiClient } from 'twenty-client-sdk/core'; - -type CalendarEventRecordingState = { - transcript: unknown; - videoFile: CalendarEventRecordingVideoFile | undefined; - isCalendarEventRecordingQueryLoading: boolean; - errorMessage: string | undefined; -}; - -type CalendarEventRecordingVideoFile = { - fileId: string; - label: string | null; - url: string | null; - extension: string | null; -}; - -type CalendarEventRecordingCallRecordingNode = { - id: string; - transcript: unknown; - video: CalendarEventRecordingVideoFile[] | null; -}; - -type CalendarEventRecordingCallRecordingEdge = { - node: CalendarEventRecordingCallRecordingNode; -}; - -const CALENDAR_EVENT_RECORDING_LOOKUP_LIMIT = 10; -const CALENDAR_EVENT_RECORDING_ERROR_MESSAGE = 'Please try again later.'; - -export const useCalendarEventRecording = ( - calendarEventId: string | undefined, -): CalendarEventRecordingState => { - const [state, setState] = useState({ - transcript: undefined, - videoFile: undefined, - isCalendarEventRecordingQueryLoading: !isUndefined(calendarEventId), - errorMessage: undefined, - }); - - useEffect(() => { - if (isUndefined(calendarEventId)) { - setState({ - transcript: undefined, - videoFile: undefined, - isCalendarEventRecordingQueryLoading: false, - errorMessage: undefined, - }); - return; - } - - let cancelled = false; - - const fetchRecording = async () => { - setState({ - transcript: undefined, - videoFile: undefined, - isCalendarEventRecordingQueryLoading: true, - errorMessage: undefined, - }); - - try { - const client = new CoreApiClient(); - const queryResult = await client.query({ - callRecordings: { - __args: { - filter: { calendarEventId: { eq: calendarEventId } }, - orderBy: [{ startedAt: 'DescNullsLast' }], - first: CALENDAR_EVENT_RECORDING_LOOKUP_LIMIT, - }, - edges: { - node: { - id: true, - transcript: true, - video: { - fileId: true, - label: true, - url: true, - extension: true, - }, - }, - }, - }, - }); - - if (cancelled) { - return; - } - - const callRecordingEdges = (queryResult.callRecordings?.edges ?? - []) as CalendarEventRecordingCallRecordingEdge[]; - const callRecordingNodes = callRecordingEdges.map( - (callRecordingEdge) => callRecordingEdge.node, - ); - const callRecordingNode = selectCalendarEventRecording( - callRecordingNodes, - ); - - setState({ - transcript: callRecordingNode?.transcript ?? undefined, - videoFile: isUndefined(callRecordingNode) - ? undefined - : getVideoFile(callRecordingNode), - isCalendarEventRecordingQueryLoading: false, - errorMessage: undefined, - }); - } catch { - if (cancelled) { - return; - } - - setState({ - transcript: undefined, - videoFile: undefined, - isCalendarEventRecordingQueryLoading: false, - errorMessage: CALENDAR_EVENT_RECORDING_ERROR_MESSAGE, - }); - } - }; - - fetchRecording(); - - return () => { - cancelled = true; - }; - }, [calendarEventId]); - - return state; -}; - -const hasTranscript = ( - callRecordingNode: CalendarEventRecordingCallRecordingNode, -): boolean => - !isUndefined(callRecordingNode.transcript) && - callRecordingNode.transcript !== null; - -const getVideoFile = ( - callRecordingNode: CalendarEventRecordingCallRecordingNode, -): CalendarEventRecordingVideoFile | undefined => - callRecordingNode.video?.find( - (videoFile) => !isUndefined(videoFile.url) && videoFile.url !== null, - ); - -const selectCalendarEventRecording = ( - callRecordingNodes: CalendarEventRecordingCallRecordingNode[], -): CalendarEventRecordingCallRecordingNode | undefined => - callRecordingNodes.find( - (callRecordingNode) => - hasTranscript(callRecordingNode) && - !isUndefined(getVideoFile(callRecordingNode)), - ) ?? - callRecordingNodes.find(hasTranscript) ?? - callRecordingNodes.find( - (callRecordingNode) => !isUndefined(getVideoFile(callRecordingNode)), - ); diff --git a/packages/twenty-apps/public/twenty-meeting-bot/src/front-components/types/calendar-event-participant-by-speaker-name.type.ts b/packages/twenty-apps/public/twenty-meeting-bot/src/front-components/types/calendar-event-participant-by-speaker-name.type.ts deleted file mode 100644 index c6fbe5e000c75..0000000000000 --- a/packages/twenty-apps/public/twenty-meeting-bot/src/front-components/types/calendar-event-participant-by-speaker-name.type.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { type CalendarEventRecordingParticipant } from 'src/front-components/types/calendar-event-recording-participant.type'; - -export type CalendarEventParticipantBySpeakerName = Map< - string, - CalendarEventRecordingParticipant ->; diff --git a/packages/twenty-apps/public/twenty-meeting-bot/src/front-components/types/calendar-event-recording-participant.type.ts b/packages/twenty-apps/public/twenty-meeting-bot/src/front-components/types/calendar-event-recording-participant.type.ts deleted file mode 100644 index ec760f1bb6065..0000000000000 --- a/packages/twenty-apps/public/twenty-meeting-bot/src/front-components/types/calendar-event-recording-participant.type.ts +++ /dev/null @@ -1,7 +0,0 @@ -export type CalendarEventRecordingParticipant = { - id: string; - avatarUrl: string | undefined; - displayName: string | undefined; - nameCandidates: string[]; - placeholderColorSeed: string; -}; diff --git a/packages/twenty-apps/public/twenty-meeting-bot/src/front-components/types/transcript-entry.type.ts b/packages/twenty-apps/public/twenty-meeting-bot/src/front-components/types/transcript-entry.type.ts deleted file mode 100644 index 0be254067011e..0000000000000 --- a/packages/twenty-apps/public/twenty-meeting-bot/src/front-components/types/transcript-entry.type.ts +++ /dev/null @@ -1,13 +0,0 @@ -export type TranscriptWord = { - text: string; - startSeconds: number | undefined; - endSeconds: number | undefined; -}; - -export type TranscriptEntry = { - speakerName: string; - startSeconds: number | undefined; - endSeconds: number | undefined; - text: string; - words: TranscriptWord[]; -}; diff --git a/packages/twenty-apps/public/twenty-meeting-bot/src/front-components/utils/__tests__/find-active-transcript-entry-index.test.ts b/packages/twenty-apps/public/twenty-meeting-bot/src/front-components/utils/__tests__/find-active-transcript-entry-index.test.ts deleted file mode 100644 index 5616dbef333ea..0000000000000 --- a/packages/twenty-apps/public/twenty-meeting-bot/src/front-components/utils/__tests__/find-active-transcript-entry-index.test.ts +++ /dev/null @@ -1,66 +0,0 @@ -import { describe, expect, it } from 'vitest'; - -import { type TranscriptEntry } from 'src/front-components/types/transcript-entry.type'; -import { findActiveTranscriptEntryIndex } from 'src/front-components/utils/find-active-transcript-entry-index.util'; - -const makeTranscriptEntry = ({ - startSeconds, - endSeconds, -}: { - startSeconds: number | undefined; - endSeconds: number | undefined; -}): TranscriptEntry => ({ - speakerName: 'Ada Lovelace', - startSeconds, - endSeconds, - text: 'Hello', - words: [{ text: 'Hello', startSeconds, endSeconds }], -}); - -describe('findActiveTranscriptEntryIndex', () => { - it('does not keep an open-ended entry active after the next entry starts', () => { - expect( - findActiveTranscriptEntryIndex( - [ - makeTranscriptEntry({ startSeconds: 1, endSeconds: undefined }), - makeTranscriptEntry({ startSeconds: 10, endSeconds: 20 }), - ], - 25, - ), - ).toBe(-1); - }); - - it('uses the next known start as the boundary for entries without an end time', () => { - expect( - findActiveTranscriptEntryIndex( - [ - makeTranscriptEntry({ startSeconds: 1, endSeconds: undefined }), - makeTranscriptEntry({ startSeconds: 10, endSeconds: 20 }), - ], - 9, - ), - ).toBe(0); - - expect( - findActiveTranscriptEntryIndex( - [ - makeTranscriptEntry({ startSeconds: 1, endSeconds: undefined }), - makeTranscriptEntry({ startSeconds: 10, endSeconds: 20 }), - ], - 10, - ), - ).toBe(1); - }); - - it('keeps the final open-ended entry active after it starts', () => { - expect( - findActiveTranscriptEntryIndex( - [ - makeTranscriptEntry({ startSeconds: 1, endSeconds: 2 }), - makeTranscriptEntry({ startSeconds: 10, endSeconds: undefined }), - ], - 25, - ), - ).toBe(1); - }); -}); diff --git a/packages/twenty-apps/public/twenty-meeting-bot/src/front-components/utils/__tests__/format-transcript-timestamp.test.ts b/packages/twenty-apps/public/twenty-meeting-bot/src/front-components/utils/__tests__/format-transcript-timestamp.test.ts deleted file mode 100644 index 79eddbd8decc6..0000000000000 --- a/packages/twenty-apps/public/twenty-meeting-bot/src/front-components/utils/__tests__/format-transcript-timestamp.test.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { describe, expect, it } from 'vitest'; - -import { formatTranscriptTimestamp } from 'src/front-components/utils/format-transcript-timestamp.util'; - -describe('formatTranscriptTimestamp', () => { - it('formats sub-hour durations as minutes and padded seconds', () => { - expect(formatTranscriptTimestamp(0)).toBe('0:00'); - expect(formatTranscriptTimestamp(5)).toBe('0:05'); - expect(formatTranscriptTimestamp(65)).toBe('1:05'); - expect(formatTranscriptTimestamp(3599)).toBe('59:59'); - }); - - it('adds an hour part with padded minutes past one hour', () => { - expect(formatTranscriptTimestamp(3600)).toBe('1:00:00'); - expect(formatTranscriptTimestamp(3725)).toBe('1:02:05'); - expect(formatTranscriptTimestamp(7322)).toBe('2:02:02'); - }); - - it('floors fractional seconds', () => { - expect(formatTranscriptTimestamp(1.9)).toBe('0:01'); - expect(formatTranscriptTimestamp(59.999)).toBe('0:59'); - }); - - it('clamps negative and non-finite input to zero', () => { - expect(formatTranscriptTimestamp(-12)).toBe('0:00'); - expect(formatTranscriptTimestamp(Number.NaN)).toBe('0:00'); - expect(formatTranscriptTimestamp(Number.POSITIVE_INFINITY)).toBe('0:00'); - }); -}); diff --git a/packages/twenty-apps/public/twenty-meeting-bot/src/front-components/utils/__tests__/get-speaker-name-match-keys.test.ts b/packages/twenty-apps/public/twenty-meeting-bot/src/front-components/utils/__tests__/get-speaker-name-match-keys.test.ts deleted file mode 100644 index 32711416bdd0a..0000000000000 --- a/packages/twenty-apps/public/twenty-meeting-bot/src/front-components/utils/__tests__/get-speaker-name-match-keys.test.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { describe, expect, it } from 'vitest'; - -import { getSpeakerNameMatchKeys } from 'src/front-components/utils/get-speaker-name-match-keys.util'; - -describe('getSpeakerNameMatchKeys', () => { - it('matches transcript full names to compact calendar aliases', () => { - expect(getSpeakerNameMatchKeys('Martin Muller')).toContain('martmull'); - expect(getSpeakerNameMatchKeys('Martmull92')).toContain('martmull'); - }); - - it('keeps exact normalized full names available for regular participant names', () => { - expect(getSpeakerNameMatchKeys('Nitin Koche')).toEqual([ - 'nitin koche', - 'nitinkoche', - 'nitikoch', - ]); - }); - - it('folds accents before generating compact match keys', () => { - expect(getSpeakerNameMatchKeys('Martin Müller')).toContain('martmull'); - }); -}); diff --git a/packages/twenty-apps/public/twenty-meeting-bot/src/front-components/utils/__tests__/parse-transcript-entries.test.ts b/packages/twenty-apps/public/twenty-meeting-bot/src/front-components/utils/__tests__/parse-transcript-entries.test.ts deleted file mode 100644 index 998896dc270b4..0000000000000 --- a/packages/twenty-apps/public/twenty-meeting-bot/src/front-components/utils/__tests__/parse-transcript-entries.test.ts +++ /dev/null @@ -1,162 +0,0 @@ -import { describe, expect, it } from 'vitest'; - -import { parseTranscriptEntries } from 'src/front-components/utils/parse-transcript-entries.util'; - -describe('parseTranscriptEntries', () => { - it('parses diarized entries into speaker, start time, and joined text', () => { - expect( - parseTranscriptEntries([ - { - participant: { id: 100, name: 'Ada Lovelace' }, - words: [ - { - text: 'Hello', - start_timestamp: { - relative: 1.2, - absolute: '2026-06-12T10:00:01Z', - }, - end_timestamp: { - relative: 1.6, - absolute: '2026-06-12T10:00:01Z', - }, - }, - { - text: 'there', - start_timestamp: { - relative: 1.7, - absolute: '2026-06-12T10:00:02Z', - }, - end_timestamp: { - relative: 2.1, - absolute: '2026-06-12T10:00:02Z', - }, - }, - ], - }, - { - participant: { id: 101, name: 'Grace Hopper' }, - words: [ - { - text: 'Hi', - start_timestamp: { - relative: 3.4, - absolute: '2026-06-12T10:00:03Z', - }, - }, - ], - }, - ]), - ).toEqual([ - { - speakerName: 'Ada Lovelace', - startSeconds: 1.2, - endSeconds: 2.1, - text: 'Hello there', - words: [ - { text: 'Hello', startSeconds: 1.2, endSeconds: 1.6 }, - { text: 'there', startSeconds: 1.7, endSeconds: 2.1 }, - ], - }, - { - speakerName: 'Grace Hopper', - startSeconds: 3.4, - endSeconds: undefined, - text: 'Hi', - words: [{ text: 'Hi', startSeconds: 3.4, endSeconds: undefined }], - }, - ]); - }); - - it('falls back to an unknown speaker when the participant has no name', () => { - expect( - parseTranscriptEntries([ - { participant: { id: 100, name: null }, words: [{ text: 'Hello' }] }, - { words: [{ text: 'Hi' }] }, - ]), - ).toEqual([ - { - speakerName: 'Unknown speaker', - startSeconds: undefined, - endSeconds: undefined, - text: 'Hello', - words: [ - { text: 'Hello', startSeconds: undefined, endSeconds: undefined }, - ], - }, - { - speakerName: 'Unknown speaker', - startSeconds: undefined, - endSeconds: undefined, - text: 'Hi', - words: [{ text: 'Hi', startSeconds: undefined, endSeconds: undefined }], - }, - ]); - }); - - it('returns an undefined start time when the first word has no relative timestamp', () => { - expect( - parseTranscriptEntries([ - { - participant: { name: 'Ada Lovelace' }, - words: [ - { - text: 'Hello', - start_timestamp: { absolute: '2026-06-12T10:00:01Z' }, - }, - ], - }, - ]), - ).toEqual([ - { - speakerName: 'Ada Lovelace', - startSeconds: undefined, - endSeconds: undefined, - text: 'Hello', - words: [ - { text: 'Hello', startSeconds: undefined, endSeconds: undefined }, - ], - }, - ]); - }); - - it('skips entries without usable words instead of failing the whole transcript', () => { - expect( - parseTranscriptEntries([ - { participant: { name: 'Ada Lovelace' }, words: [] }, - { participant: { name: 'Grace Hopper' } }, - { - participant: { name: 'Alan Turing' }, - words: [{ text: ' ' }, 42, null], - }, - { participant: { name: 'Joan Clarke' }, words: [{ text: 'Kept' }] }, - 'not an entry', - ]), - ).toEqual([ - { - speakerName: 'Joan Clarke', - startSeconds: undefined, - endSeconds: undefined, - text: 'Kept', - words: [ - { text: 'Kept', startSeconds: undefined, endSeconds: undefined }, - ], - }, - ]); - }); - - it('returns an empty list for an empty transcript array', () => { - expect(parseTranscriptEntries([])).toEqual([]); - }); - - it('returns undefined for values that are not a diarized transcript array', () => { - expect(parseTranscriptEntries(null)).toBeUndefined(); - expect(parseTranscriptEntries(undefined)).toBeUndefined(); - expect(parseTranscriptEntries('transcript text')).toBeUndefined(); - expect( - parseTranscriptEntries({ - recallTranscriptId: 'recall-transcript-1', - status: 'PENDING', - }), - ).toBeUndefined(); - }); -}); diff --git a/packages/twenty-apps/public/twenty-meeting-bot/src/front-components/utils/build-calendar-event-participant-by-speaker-name.util.ts b/packages/twenty-apps/public/twenty-meeting-bot/src/front-components/utils/build-calendar-event-participant-by-speaker-name.util.ts deleted file mode 100644 index cb70604e1a6eb..0000000000000 --- a/packages/twenty-apps/public/twenty-meeting-bot/src/front-components/utils/build-calendar-event-participant-by-speaker-name.util.ts +++ /dev/null @@ -1,45 +0,0 @@ -import { isUndefined } from '@sniptt/guards'; - -import { type CalendarEventParticipantBySpeakerName } from 'src/front-components/types/calendar-event-participant-by-speaker-name.type'; -import { type CalendarEventRecordingParticipant } from 'src/front-components/types/calendar-event-recording-participant.type'; -import { getSpeakerNameMatchKeys } from 'src/front-components/utils/get-speaker-name-match-keys.util'; - -export const buildCalendarEventParticipantBySpeakerName = ( - calendarEventParticipants: CalendarEventRecordingParticipant[], -): CalendarEventParticipantBySpeakerName => { - const calendarEventParticipantBySpeakerName: CalendarEventParticipantBySpeakerName = - new Map(); - const ambiguousSpeakerNameMatchKeys = new Set(); - - for (const calendarEventParticipant of calendarEventParticipants) { - for (const nameCandidate of calendarEventParticipant.nameCandidates) { - const speakerNameMatchKeys = getSpeakerNameMatchKeys(nameCandidate); - - for (const speakerNameMatchKey of speakerNameMatchKeys) { - const matchingCalendarEventParticipant = - calendarEventParticipantBySpeakerName.get(speakerNameMatchKey); - - if (ambiguousSpeakerNameMatchKeys.has(speakerNameMatchKey)) { - continue; - } - - if (isUndefined(matchingCalendarEventParticipant)) { - calendarEventParticipantBySpeakerName.set( - speakerNameMatchKey, - calendarEventParticipant, - ); - continue; - } - - if ( - matchingCalendarEventParticipant.id !== calendarEventParticipant.id - ) { - calendarEventParticipantBySpeakerName.delete(speakerNameMatchKey); - ambiguousSpeakerNameMatchKeys.add(speakerNameMatchKey); - } - } - } - } - - return calendarEventParticipantBySpeakerName; -}; diff --git a/packages/twenty-apps/public/twenty-meeting-bot/src/front-components/utils/find-active-transcript-entry-index.util.ts b/packages/twenty-apps/public/twenty-meeting-bot/src/front-components/utils/find-active-transcript-entry-index.util.ts deleted file mode 100644 index 54fcd26283a9f..0000000000000 --- a/packages/twenty-apps/public/twenty-meeting-bot/src/front-components/utils/find-active-transcript-entry-index.util.ts +++ /dev/null @@ -1,77 +0,0 @@ -import { isUndefined } from '@sniptt/guards'; - -import { type TranscriptEntry } from 'src/front-components/types/transcript-entry.type'; - -export const findActiveTranscriptEntryIndex = ( - entries: TranscriptEntry[], - currentTimeSeconds: number, -): number => { - for (let entryIndex = entries.length - 1; entryIndex >= 0; entryIndex--) { - const entry = entries[entryIndex]; - - if ( - isTranscriptEntryActive({ - entries, - entry, - entryIndex, - currentTimeSeconds, - }) - ) { - return entryIndex; - } - } - - return -1; -}; - -const isTranscriptEntryActive = ({ - entries, - entry, - entryIndex, - currentTimeSeconds, -}: { - entries: TranscriptEntry[]; - entry: TranscriptEntry; - entryIndex: number; - currentTimeSeconds: number; -}): boolean => { - if ( - isUndefined(entry.startSeconds) || - currentTimeSeconds < entry.startSeconds - ) { - return false; - } - - if (!isUndefined(entry.endSeconds)) { - return currentTimeSeconds <= entry.endSeconds; - } - - const nextTranscriptEntryStartSeconds = findNextTranscriptEntryStartSeconds( - entries, - entryIndex, - ); - - return isUndefined(nextTranscriptEntryStartSeconds) - ? true - : currentTimeSeconds < nextTranscriptEntryStartSeconds; -}; - -const findNextTranscriptEntryStartSeconds = ( - entries: TranscriptEntry[], - entryIndex: number, -): number | undefined => { - for ( - let nextEntryIndex = entryIndex + 1; - nextEntryIndex < entries.length; - nextEntryIndex++ - ) { - const nextTranscriptEntryStartSeconds = - entries[nextEntryIndex].startSeconds; - - if (!isUndefined(nextTranscriptEntryStartSeconds)) { - return nextTranscriptEntryStartSeconds; - } - } - - return undefined; -}; diff --git a/packages/twenty-apps/public/twenty-meeting-bot/src/front-components/utils/format-transcript-timestamp.util.ts b/packages/twenty-apps/public/twenty-meeting-bot/src/front-components/utils/format-transcript-timestamp.util.ts deleted file mode 100644 index 1551203ece2a4..0000000000000 --- a/packages/twenty-apps/public/twenty-meeting-bot/src/front-components/utils/format-transcript-timestamp.util.ts +++ /dev/null @@ -1,16 +0,0 @@ -export const formatTranscriptTimestamp = (totalSeconds: number): string => { - const safeSeconds = Number.isFinite(totalSeconds) - ? Math.max(0, Math.floor(totalSeconds)) - : 0; - - const hours = Math.floor(safeSeconds / 3600); - const minutes = Math.floor((safeSeconds % 3600) / 60); - const seconds = safeSeconds % 60; - const paddedSeconds = String(seconds).padStart(2, '0'); - - if (hours > 0) { - return `${hours}:${String(minutes).padStart(2, '0')}:${paddedSeconds}`; - } - - return `${minutes}:${paddedSeconds}`; -}; diff --git a/packages/twenty-apps/public/twenty-meeting-bot/src/front-components/utils/get-absolute-avatar-url.util.ts b/packages/twenty-apps/public/twenty-meeting-bot/src/front-components/utils/get-absolute-avatar-url.util.ts deleted file mode 100644 index 404717f70c697..0000000000000 --- a/packages/twenty-apps/public/twenty-meeting-bot/src/front-components/utils/get-absolute-avatar-url.util.ts +++ /dev/null @@ -1,48 +0,0 @@ -// Duplicates minimal front image URL logic for this app. -// Remove once shared front utilities can be imported safely in front components. -import { isNonEmptyString } from 'src/logic-functions/utils/is-non-empty-string.util'; - -type GetImageAbsoluteUrlArgs = { - imageUrl: string; - baseUrl: string; -}; - -const getImageAbsoluteUrl = ({ - imageUrl, - baseUrl, -}: GetImageAbsoluteUrlArgs): string => { - const lowerCaseImageUrl = imageUrl.toLowerCase(); - const isAlreadyAbsoluteUrl = - ['http:', 'https:', 'data:', 'blob:'].some((scheme) => - lowerCaseImageUrl.startsWith(scheme), - ) || imageUrl.startsWith('//'); - - if (isAlreadyAbsoluteUrl) { - return imageUrl; - } - - if (imageUrl.startsWith('/')) { - return new URL(`/files${imageUrl}`, baseUrl).toString(); - } - - return new URL(`/files/${imageUrl}`, baseUrl).toString(); -}; - -export const getAbsoluteAvatarUrl = ( - avatarUrl: string | null | undefined, -): string | undefined => { - if (!isNonEmptyString(avatarUrl)) { - return undefined; - } - - const apiBaseUrl = process.env.TWENTY_API_URL; - - if (!isNonEmptyString(apiBaseUrl)) { - return avatarUrl.trim(); - } - - return getImageAbsoluteUrl({ - imageUrl: avatarUrl.trim(), - baseUrl: apiBaseUrl, - }); -}; diff --git a/packages/twenty-apps/public/twenty-meeting-bot/src/front-components/utils/get-calendar-event-participant-for-speaker-name.util.ts b/packages/twenty-apps/public/twenty-meeting-bot/src/front-components/utils/get-calendar-event-participant-for-speaker-name.util.ts deleted file mode 100644 index 261f4dc79bb9f..0000000000000 --- a/packages/twenty-apps/public/twenty-meeting-bot/src/front-components/utils/get-calendar-event-participant-for-speaker-name.util.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { isUndefined } from '@sniptt/guards'; - -import { type CalendarEventParticipantBySpeakerName } from 'src/front-components/types/calendar-event-participant-by-speaker-name.type'; -import { type CalendarEventRecordingParticipant } from 'src/front-components/types/calendar-event-recording-participant.type'; -import { getSpeakerNameMatchKeys } from 'src/front-components/utils/get-speaker-name-match-keys.util'; - -export const getCalendarEventParticipantForSpeakerName = ({ - speakerName, - calendarEventParticipantBySpeakerName, -}: { - speakerName: string; - calendarEventParticipantBySpeakerName: CalendarEventParticipantBySpeakerName; -}): CalendarEventRecordingParticipant | undefined => { - for (const speakerNameMatchKey of getSpeakerNameMatchKeys(speakerName)) { - const calendarEventParticipant = - calendarEventParticipantBySpeakerName.get(speakerNameMatchKey); - - if (!isUndefined(calendarEventParticipant)) { - return calendarEventParticipant; - } - } - - return undefined; -}; diff --git a/packages/twenty-apps/public/twenty-meeting-bot/src/front-components/utils/get-speaker-name-match-keys.util.ts b/packages/twenty-apps/public/twenty-meeting-bot/src/front-components/utils/get-speaker-name-match-keys.util.ts deleted file mode 100644 index c570effd71871..0000000000000 --- a/packages/twenty-apps/public/twenty-meeting-bot/src/front-components/utils/get-speaker-name-match-keys.util.ts +++ /dev/null @@ -1,64 +0,0 @@ -import { isNonEmptyString } from 'src/logic-functions/utils/is-non-empty-string.util'; - -const MINIMUM_FUZZY_MATCH_KEY_LENGTH = 5; - -export const getSpeakerNameMatchKeys = (speakerName: string): string[] => { - const normalizedSpeakerName = normalizeSpeakerName(speakerName); - const compactSpeakerName = getCompactSpeakerName(normalizedSpeakerName); - const compactSpeakerNameWithoutDigits = compactSpeakerName.replace(/\d/g, ''); - const abbreviatedSpeakerNameMatchKey = - getAbbreviatedSpeakerNameMatchKey(normalizedSpeakerName); - - return [ - ...new Set( - [ - normalizedSpeakerName, - compactSpeakerName, - compactSpeakerNameWithoutDigits, - abbreviatedSpeakerNameMatchKey, - ].filter(isSpeakerNameMatchKey), - ), - ]; -}; - -const normalizeSpeakerName = (speakerName: string): string => - speakerName - .trim() - .normalize('NFD') - .replace(/[\u0300-\u036f]/g, '') - .toLocaleLowerCase(); - -const getCompactSpeakerName = (speakerName: string): string => - normalizeSpeakerName(speakerName).replace(/[^a-z0-9]/g, ''); - -const getAbbreviatedSpeakerNameMatchKey = ( - speakerName: string, -): string | undefined => { - const speakerNameParts = normalizeSpeakerName(speakerName) - .split(/\s+/) - .map(getCompactSpeakerName) - .filter(isNonEmptyString); - - if (speakerNameParts.length < 2) { - return undefined; - } - - const firstSpeakerNamePart = speakerNameParts[0]; - const lastSpeakerNamePart = speakerNameParts[speakerNameParts.length - 1]; - const abbreviatedSpeakerNameMatchKey = `${firstSpeakerNamePart.slice( - 0, - 4, - )}${lastSpeakerNamePart.slice(0, 4)}`; - - return abbreviatedSpeakerNameMatchKey.length >= - MINIMUM_FUZZY_MATCH_KEY_LENGTH - ? abbreviatedSpeakerNameMatchKey - : undefined; -}; - -const isSpeakerNameMatchKey = ( - speakerNameMatchKey: string | undefined, -): speakerNameMatchKey is string => - isNonEmptyString(speakerNameMatchKey) && - (speakerNameMatchKey.includes(' ') || - speakerNameMatchKey.length >= MINIMUM_FUZZY_MATCH_KEY_LENGTH); diff --git a/packages/twenty-apps/public/twenty-meeting-bot/src/front-components/utils/get-video-file-extension.util.ts b/packages/twenty-apps/public/twenty-meeting-bot/src/front-components/utils/get-video-file-extension.util.ts deleted file mode 100644 index 7dbfd4efab918..0000000000000 --- a/packages/twenty-apps/public/twenty-meeting-bot/src/front-components/utils/get-video-file-extension.util.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { isUndefined } from '@sniptt/guards'; - -import { isNonEmptyString } from 'src/logic-functions/utils/is-non-empty-string.util'; - -export const getVideoFileExtension = ({ - extension, - label, -}: { - extension: string | null; - label: string | null; -}): string | undefined => { - const labelParts = label?.split('.'); - const videoFileExtension = - extension ?? - (isUndefined(labelParts) ? undefined : labelParts[labelParts.length - 1]); - const normalizedVideoFileExtension = videoFileExtension - ?.toLowerCase() - .replace(/^\./, ''); - - return isNonEmptyString(normalizedVideoFileExtension) - ? normalizedVideoFileExtension - : undefined; -}; diff --git a/packages/twenty-apps/public/twenty-meeting-bot/src/front-components/utils/parse-transcript-entries.util.ts b/packages/twenty-apps/public/twenty-meeting-bot/src/front-components/utils/parse-transcript-entries.util.ts deleted file mode 100644 index e35a79b6e5e3d..0000000000000 --- a/packages/twenty-apps/public/twenty-meeting-bot/src/front-components/utils/parse-transcript-entries.util.ts +++ /dev/null @@ -1,87 +0,0 @@ -import { isArray, isNumber, isUndefined } from '@sniptt/guards'; - -import { - type TranscriptEntry, - type TranscriptWord, -} from 'src/front-components/types/transcript-entry.type'; -import { asRecord } from 'src/logic-functions/utils/as-record.util'; -import { isNonEmptyString } from 'src/logic-functions/utils/is-non-empty-string.util'; - -type TranscriptRecord = NonNullable>; - -const isTranscriptRecord = ( - candidate: TranscriptRecord | undefined, -): candidate is TranscriptRecord => !isUndefined(candidate); - -const readRelativeTimestamp = ( - timestamp: TranscriptRecord | undefined, -): number | undefined => { - const relativeTimestamp = timestamp?.relative; - - return isNumber(relativeTimestamp) && Number.isFinite(relativeTimestamp) - ? relativeTimestamp - : undefined; -}; - -const readTranscriptWord = ( - candidate: TranscriptRecord, -): TranscriptWord | undefined => { - if (!isNonEmptyString(candidate.text)) { - return undefined; - } - - return { - text: candidate.text.trim(), - startSeconds: readRelativeTimestamp(asRecord(candidate.start_timestamp)), - endSeconds: readRelativeTimestamp(asRecord(candidate.end_timestamp)), - }; -}; - -const readSpeakerName = ( - participant: TranscriptRecord | undefined, -): string => { - const name = participant?.name; - - return isNonEmptyString(name) ? name.trim() : 'Unknown speaker'; -}; - -const readTranscriptEntry = ( - candidate: TranscriptRecord, -): TranscriptEntry | undefined => { - if (!isArray(candidate.words)) { - return undefined; - } - - const words = candidate.words - .map(asRecord) - .filter(isTranscriptRecord) - .map(readTranscriptWord) - .filter((word): word is TranscriptWord => !isUndefined(word)); - - if (words.length === 0) { - return undefined; - } - - return { - speakerName: readSpeakerName(asRecord(candidate.participant)), - startSeconds: words[0].startSeconds, - endSeconds: words[words.length - 1].endSeconds, - text: words.map((word) => word.text).join(' '), - words, - }; -}; - -// Undefined means the value is not a diarized transcript; malformed entries are skipped, not fatal. -export const parseTranscriptEntries = ( - transcript: unknown, -): TranscriptEntry[] | undefined => { - if (!isArray(transcript)) { - return undefined; - } - - return transcript - .map(asRecord) - .filter(isTranscriptRecord) - .map(readTranscriptEntry) - .filter((entry): entry is TranscriptEntry => !isUndefined(entry)); -}; diff --git a/packages/twenty-apps/public/twenty-meeting-bot/src/logic-functions/__tests__/recall-webhook.test.ts b/packages/twenty-apps/public/twenty-meeting-bot/src/logic-functions/__tests__/recall-webhook.test.ts deleted file mode 100644 index 3a20df91e5cd9..0000000000000 --- a/packages/twenty-apps/public/twenty-meeting-bot/src/logic-functions/__tests__/recall-webhook.test.ts +++ /dev/null @@ -1,194 +0,0 @@ -import { createHmac } from 'crypto'; - -import { beforeEach, describe, expect, it, vi } from 'vitest'; - -import recallWebhookLogicFunction, { - recallWebhookRouteHandler, -} from 'src/logic-functions/recall-webhook'; - -const getApplicationVariableValueMock = vi.hoisted(() => vi.fn()); -const handleRecallWebhookMock = vi.hoisted(() => vi.fn()); - -vi.mock( - 'src/logic-functions/utils/get-application-variable-value.util', - () => ({ - getApplicationVariableValue: getApplicationVariableValueMock, - }), -); - -vi.mock('src/logic-functions/flows/handle-recall-webhook.util', () => ({ - handleRecallWebhook: handleRecallWebhookMock, -})); - -vi.mock('twenty-client-sdk/core', () => ({ - CoreApiClient: vi.fn(), -})); - -const SECRET_BYTES = Buffer.from('entry-test-secret'); -const SECRET = `whsec_${SECRET_BYTES.toString('base64')}`; -const WORKSPACE_ID = '123e4567-e89b-12d3-a456-426614174000'; - -type RecallWebhookRoutePayload = Parameters< - typeof recallWebhookRouteHandler ->[0]; - -const buildRoutePayload = ( - overrides: Partial, -): RecallWebhookRoutePayload => - ({ - headers: {}, - ...overrides, - }) as RecallWebhookRoutePayload; - -const buildSignedHeaders = (rawBody: string): Record => { - const webhookId = 'msg_entry_test'; - const webhookTimestamp = Math.floor(Date.now() / 1000).toString(); - const signature = createHmac('sha256', SECRET_BYTES) - .update(`${webhookId}.${webhookTimestamp}.${rawBody}`) - .digest('base64'); - - return { - 'webhook-id': webhookId, - 'webhook-timestamp': webhookTimestamp, - 'webhook-signature': `v1,${signature}`, - }; -}; - -const buildRecordingDoneWebhookBody = () => ({ - event: 'recording.done', - data: { - bot: { - id: 'recall-bot-1', - metadata: { - twentyWorkspaceId: WORKSPACE_ID, - }, - }, - recording: { - id: 'recall-recording-1', - }, - }, -}); - -describe('recallWebhookRouteHandler', () => { - beforeEach(() => { - vi.spyOn(console, 'error').mockImplementation(() => {}); - getApplicationVariableValueMock.mockReset(); - getApplicationVariableValueMock.mockReturnValue(SECRET); - handleRecallWebhookMock.mockReset(); - handleRecallWebhookMock.mockResolvedValue({ status: 'updated' }); - }); - - it('declares a server webhook resolver for Recall bot workspace metadata', () => { - expect(recallWebhookLogicFunction.success).toBe(true); - expect( - recallWebhookLogicFunction.config.httpRouteTriggerSettings, - ).toBeUndefined(); - expect( - recallWebhookLogicFunction.config.serverWebhookTriggerSettings, - ).toEqual({ - workspaceIdResolver: { - source: 'body', - path: 'data.bot.metadata.twentyWorkspaceId', - }, - forwardedRequestHeaders: [ - 'webhook-id', - 'webhook-timestamp', - 'webhook-signature', - 'svix-id', - 'svix-timestamp', - 'svix-signature', - ], - }); - }); - - it('responds 500 when the webhook secret is not configured', async () => { - getApplicationVariableValueMock.mockReturnValue(undefined); - - const result = await recallWebhookRouteHandler( - buildRoutePayload({ rawBody: '{}', body: {} }), - ); - - expect(result).toMatchObject({ - __twentyHttpResponse: true, - status: 500, - body: { - error: expect.stringContaining('RECALL_WEBHOOK_SECRET'), - }, - }); - }); - - it('responds 500 when the raw body is not forwarded', async () => { - const result = await recallWebhookRouteHandler( - buildRoutePayload({ body: {} }), - ); - - expect(result).toMatchObject({ - __twentyHttpResponse: true, - status: 500, - body: { - error: expect.stringContaining('Raw request body'), - }, - }); - }); - - it('responds 401 when the signature is invalid', async () => { - const result = await recallWebhookRouteHandler( - buildRoutePayload({ - rawBody: '{}', - body: {}, - headers: { - 'webhook-id': 'msg_entry_test', - 'webhook-timestamp': Math.floor(Date.now() / 1000).toString(), - 'webhook-signature': 'v1,not-a-real-signature', - }, - }), - ); - - expect(result).toMatchObject({ - __twentyHttpResponse: true, - status: 401, - body: { - error: expect.stringContaining('Invalid webhook signature'), - }, - }); - }); - - it('responds 400 when a correctly signed payload is empty', async () => { - const rawBody = 'null'; - - const result = await recallWebhookRouteHandler( - buildRoutePayload({ - rawBody, - body: null, - headers: buildSignedHeaders(rawBody), - }), - ); - - expect(result).toMatchObject({ - __twentyHttpResponse: true, - status: 400, - body: { - error: 'Webhook payload was empty', - }, - }); - }); - - it('dispatches a correctly signed payload to the handler', async () => { - const body = buildRecordingDoneWebhookBody(); - const rawBody = JSON.stringify(body); - - const result = await recallWebhookRouteHandler( - buildRoutePayload({ - rawBody, - body, - headers: buildSignedHeaders(rawBody), - }), - ); - - expect(handleRecallWebhookMock).toHaveBeenCalledTimes(1); - expect(handleRecallWebhookMock).toHaveBeenCalledWith( - expect.objectContaining({ body }), - ); - expect(result).toEqual({ status: 'updated' }); - }); -}); diff --git a/packages/twenty-apps/public/twenty-meeting-bot/src/logic-functions/constants/call-recording-micro-credits-per-hour.ts b/packages/twenty-apps/public/twenty-meeting-bot/src/logic-functions/constants/call-recording-micro-credits-per-hour.ts deleted file mode 100644 index 8fa84cc76f247..0000000000000 --- a/packages/twenty-apps/public/twenty-meeting-bot/src/logic-functions/constants/call-recording-micro-credits-per-hour.ts +++ /dev/null @@ -1 +0,0 @@ -export const CALL_RECORDING_MICRO_CREDITS_PER_HOUR = 1_000_000; diff --git a/packages/twenty-apps/public/twenty-meeting-bot/src/logic-functions/constants/call-recording-request-status.ts b/packages/twenty-apps/public/twenty-meeting-bot/src/logic-functions/constants/call-recording-request-status.ts deleted file mode 100644 index 0413760cbc9b8..0000000000000 --- a/packages/twenty-apps/public/twenty-meeting-bot/src/logic-functions/constants/call-recording-request-status.ts +++ /dev/null @@ -1,5 +0,0 @@ -// Mirrors the core select options; guarded by the schema integration test. -export enum CallRecordingRequestStatus { - REQUESTED = 'REQUESTED', - CANCELED = 'CANCELED', -} diff --git a/packages/twenty-apps/public/twenty-meeting-bot/src/logic-functions/constants/call-recording-status.ts b/packages/twenty-apps/public/twenty-meeting-bot/src/logic-functions/constants/call-recording-status.ts deleted file mode 100644 index 60b8b6f14f3e4..0000000000000 --- a/packages/twenty-apps/public/twenty-meeting-bot/src/logic-functions/constants/call-recording-status.ts +++ /dev/null @@ -1,9 +0,0 @@ -// Mirrors the core select options; guarded by the schema integration test. -export enum CallRecordingStatus { - SCHEDULED = 'SCHEDULED', - JOINING = 'JOINING', - RECORDING = 'RECORDING', - PROCESSING = 'PROCESSING', - COMPLETED = 'COMPLETED', - FAILED = 'FAILED', -} diff --git a/packages/twenty-apps/public/twenty-meeting-bot/src/logic-functions/constants/default-meeting-bot-join-early-minutes.ts b/packages/twenty-apps/public/twenty-meeting-bot/src/logic-functions/constants/default-meeting-bot-join-early-minutes.ts deleted file mode 100644 index 4af45b74e94e4..0000000000000 --- a/packages/twenty-apps/public/twenty-meeting-bot/src/logic-functions/constants/default-meeting-bot-join-early-minutes.ts +++ /dev/null @@ -1 +0,0 @@ -export const DEFAULT_MEETING_BOT_JOIN_EARLY_MINUTES = 1; diff --git a/packages/twenty-apps/public/twenty-meeting-bot/src/logic-functions/constants/default-meeting-bot-name.ts b/packages/twenty-apps/public/twenty-meeting-bot/src/logic-functions/constants/default-meeting-bot-name.ts deleted file mode 100644 index 4c2a6f77e3abc..0000000000000 --- a/packages/twenty-apps/public/twenty-meeting-bot/src/logic-functions/constants/default-meeting-bot-name.ts +++ /dev/null @@ -1 +0,0 @@ -export const DEFAULT_MEETING_BOT_NAME = 'Twenty Meeting Bot'; diff --git a/packages/twenty-apps/public/twenty-meeting-bot/src/logic-functions/constants/default-meeting-bot-recording-retention-hours.ts b/packages/twenty-apps/public/twenty-meeting-bot/src/logic-functions/constants/default-meeting-bot-recording-retention-hours.ts deleted file mode 100644 index 3ec7807e428f4..0000000000000 --- a/packages/twenty-apps/public/twenty-meeting-bot/src/logic-functions/constants/default-meeting-bot-recording-retention-hours.ts +++ /dev/null @@ -1,2 +0,0 @@ -// Twenty stores ingested recording artifacts, so Recall.ai media is temporary. Keep the default below Recall.ai's 168-hour free storage window. -export const DEFAULT_MEETING_BOT_RECORDING_RETENTION_HOURS = 166; diff --git a/packages/twenty-apps/public/twenty-meeting-bot/src/logic-functions/constants/default-recall-region.ts b/packages/twenty-apps/public/twenty-meeting-bot/src/logic-functions/constants/default-recall-region.ts deleted file mode 100644 index fdafca50533cb..0000000000000 --- a/packages/twenty-apps/public/twenty-meeting-bot/src/logic-functions/constants/default-recall-region.ts +++ /dev/null @@ -1 +0,0 @@ -export const DEFAULT_RECALL_REGION = 'eu-central-1'; diff --git a/packages/twenty-apps/public/twenty-meeting-bot/src/logic-functions/constants/meeting-bot-everyone-left-timeout-seconds-env-var-name.ts b/packages/twenty-apps/public/twenty-meeting-bot/src/logic-functions/constants/meeting-bot-everyone-left-timeout-seconds-env-var-name.ts deleted file mode 100644 index 2528599282359..0000000000000 --- a/packages/twenty-apps/public/twenty-meeting-bot/src/logic-functions/constants/meeting-bot-everyone-left-timeout-seconds-env-var-name.ts +++ /dev/null @@ -1,2 +0,0 @@ -export const MEETING_BOT_EVERYONE_LEFT_TIMEOUT_SECONDS_ENV_VAR_NAME = - 'MEETING_BOT_EVERYONE_LEFT_TIMEOUT_SECONDS'; diff --git a/packages/twenty-apps/public/twenty-meeting-bot/src/logic-functions/constants/meeting-bot-everyone-left-timeout-seconds.ts b/packages/twenty-apps/public/twenty-meeting-bot/src/logic-functions/constants/meeting-bot-everyone-left-timeout-seconds.ts deleted file mode 100644 index e4c841805a5fe..0000000000000 --- a/packages/twenty-apps/public/twenty-meeting-bot/src/logic-functions/constants/meeting-bot-everyone-left-timeout-seconds.ts +++ /dev/null @@ -1 +0,0 @@ -export const MEETING_BOT_EVERYONE_LEFT_TIMEOUT_SECONDS = 2; diff --git a/packages/twenty-apps/public/twenty-meeting-bot/src/logic-functions/constants/meeting-bot-join-early-minutes-env-var-name.ts b/packages/twenty-apps/public/twenty-meeting-bot/src/logic-functions/constants/meeting-bot-join-early-minutes-env-var-name.ts deleted file mode 100644 index 7cb8ac5121f78..0000000000000 --- a/packages/twenty-apps/public/twenty-meeting-bot/src/logic-functions/constants/meeting-bot-join-early-minutes-env-var-name.ts +++ /dev/null @@ -1,2 +0,0 @@ -export const MEETING_BOT_JOIN_EARLY_MINUTES_ENV_VAR_NAME = - 'MEETING_BOT_JOIN_EARLY_MINUTES'; diff --git a/packages/twenty-apps/public/twenty-meeting-bot/src/logic-functions/constants/meeting-bot-name-env-var-name.ts b/packages/twenty-apps/public/twenty-meeting-bot/src/logic-functions/constants/meeting-bot-name-env-var-name.ts deleted file mode 100644 index efa200e15ef9c..0000000000000 --- a/packages/twenty-apps/public/twenty-meeting-bot/src/logic-functions/constants/meeting-bot-name-env-var-name.ts +++ /dev/null @@ -1 +0,0 @@ -export const MEETING_BOT_NAME_ENV_VAR_NAME = 'MEETING_BOT_NAME'; diff --git a/packages/twenty-apps/public/twenty-meeting-bot/src/logic-functions/constants/meeting-bot-noone-joined-timeout-seconds-env-var-name.ts b/packages/twenty-apps/public/twenty-meeting-bot/src/logic-functions/constants/meeting-bot-noone-joined-timeout-seconds-env-var-name.ts deleted file mode 100644 index f530a83ca4e5c..0000000000000 --- a/packages/twenty-apps/public/twenty-meeting-bot/src/logic-functions/constants/meeting-bot-noone-joined-timeout-seconds-env-var-name.ts +++ /dev/null @@ -1,2 +0,0 @@ -export const MEETING_BOT_NOONE_JOINED_TIMEOUT_SECONDS_ENV_VAR_NAME = - 'MEETING_BOT_NOONE_JOINED_TIMEOUT_SECONDS'; diff --git a/packages/twenty-apps/public/twenty-meeting-bot/src/logic-functions/constants/meeting-bot-noone-joined-timeout-seconds.ts b/packages/twenty-apps/public/twenty-meeting-bot/src/logic-functions/constants/meeting-bot-noone-joined-timeout-seconds.ts deleted file mode 100644 index 52ed2c3672a55..0000000000000 --- a/packages/twenty-apps/public/twenty-meeting-bot/src/logic-functions/constants/meeting-bot-noone-joined-timeout-seconds.ts +++ /dev/null @@ -1 +0,0 @@ -export const MEETING_BOT_NOONE_JOINED_TIMEOUT_SECONDS = 20 * 60; diff --git a/packages/twenty-apps/public/twenty-meeting-bot/src/logic-functions/constants/meeting-bot-recording-retention-hours-env-var-name.ts b/packages/twenty-apps/public/twenty-meeting-bot/src/logic-functions/constants/meeting-bot-recording-retention-hours-env-var-name.ts deleted file mode 100644 index f8a6573c9ec47..0000000000000 --- a/packages/twenty-apps/public/twenty-meeting-bot/src/logic-functions/constants/meeting-bot-recording-retention-hours-env-var-name.ts +++ /dev/null @@ -1,2 +0,0 @@ -export const MEETING_BOT_RECORDING_RETENTION_HOURS_ENV_VAR_NAME = - 'MEETING_BOT_RECORDING_RETENTION_HOURS'; diff --git a/packages/twenty-apps/public/twenty-meeting-bot/src/logic-functions/constants/meeting-bot-waiting-room-timeout-seconds-env-var-name.ts b/packages/twenty-apps/public/twenty-meeting-bot/src/logic-functions/constants/meeting-bot-waiting-room-timeout-seconds-env-var-name.ts deleted file mode 100644 index ca872670bc45a..0000000000000 --- a/packages/twenty-apps/public/twenty-meeting-bot/src/logic-functions/constants/meeting-bot-waiting-room-timeout-seconds-env-var-name.ts +++ /dev/null @@ -1,2 +0,0 @@ -export const MEETING_BOT_WAITING_ROOM_TIMEOUT_SECONDS_ENV_VAR_NAME = - 'MEETING_BOT_WAITING_ROOM_TIMEOUT_SECONDS'; diff --git a/packages/twenty-apps/public/twenty-meeting-bot/src/logic-functions/constants/meeting-bot-waiting-room-timeout-seconds.ts b/packages/twenty-apps/public/twenty-meeting-bot/src/logic-functions/constants/meeting-bot-waiting-room-timeout-seconds.ts deleted file mode 100644 index 42bfa92498212..0000000000000 --- a/packages/twenty-apps/public/twenty-meeting-bot/src/logic-functions/constants/meeting-bot-waiting-room-timeout-seconds.ts +++ /dev/null @@ -1 +0,0 @@ -export const MEETING_BOT_WAITING_ROOM_TIMEOUT_SECONDS = 1200; diff --git a/packages/twenty-apps/public/twenty-meeting-bot/src/logic-functions/constants/milliseconds-per-minute.ts b/packages/twenty-apps/public/twenty-meeting-bot/src/logic-functions/constants/milliseconds-per-minute.ts deleted file mode 100644 index c72bff9db3153..0000000000000 --- a/packages/twenty-apps/public/twenty-meeting-bot/src/logic-functions/constants/milliseconds-per-minute.ts +++ /dev/null @@ -1 +0,0 @@ -export const MILLISECONDS_PER_MINUTE = 60_000; diff --git a/packages/twenty-apps/public/twenty-meeting-bot/src/logic-functions/constants/non-terminal-call-recording-statuses.ts b/packages/twenty-apps/public/twenty-meeting-bot/src/logic-functions/constants/non-terminal-call-recording-statuses.ts deleted file mode 100644 index dd604c7e85836..0000000000000 --- a/packages/twenty-apps/public/twenty-meeting-bot/src/logic-functions/constants/non-terminal-call-recording-statuses.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { CallRecordingStatus } from 'src/logic-functions/constants/call-recording-status'; - -export const NON_TERMINAL_CALL_RECORDING_STATUSES = [ - CallRecordingStatus.SCHEDULED, - CallRecordingStatus.JOINING, - CallRecordingStatus.RECORDING, - CallRecordingStatus.PROCESSING, -] satisfies CallRecordingStatus[]; diff --git a/packages/twenty-apps/public/twenty-meeting-bot/src/logic-functions/constants/recall-api-key-env-var-name.ts b/packages/twenty-apps/public/twenty-meeting-bot/src/logic-functions/constants/recall-api-key-env-var-name.ts deleted file mode 100644 index aa5f13a0ac0d9..0000000000000 --- a/packages/twenty-apps/public/twenty-meeting-bot/src/logic-functions/constants/recall-api-key-env-var-name.ts +++ /dev/null @@ -1 +0,0 @@ -export const RECALL_API_KEY_ENV_VAR_NAME = 'RECALL_API_KEY'; diff --git a/packages/twenty-apps/public/twenty-meeting-bot/src/logic-functions/constants/recall-api-max-attempts.ts b/packages/twenty-apps/public/twenty-meeting-bot/src/logic-functions/constants/recall-api-max-attempts.ts deleted file mode 100644 index b8c6d083652a0..0000000000000 --- a/packages/twenty-apps/public/twenty-meeting-bot/src/logic-functions/constants/recall-api-max-attempts.ts +++ /dev/null @@ -1 +0,0 @@ -export const RECALL_API_MAX_ATTEMPTS = 3; diff --git a/packages/twenty-apps/public/twenty-meeting-bot/src/logic-functions/constants/recall-api-retry-delay-ms.ts b/packages/twenty-apps/public/twenty-meeting-bot/src/logic-functions/constants/recall-api-retry-delay-ms.ts deleted file mode 100644 index 06dba9a11d7d9..0000000000000 --- a/packages/twenty-apps/public/twenty-meeting-bot/src/logic-functions/constants/recall-api-retry-delay-ms.ts +++ /dev/null @@ -1 +0,0 @@ -export const RECALL_API_RETRY_DELAY_MS = 500; diff --git a/packages/twenty-apps/public/twenty-meeting-bot/src/logic-functions/constants/recall-bot-automatic-leave.ts b/packages/twenty-apps/public/twenty-meeting-bot/src/logic-functions/constants/recall-bot-automatic-leave.ts deleted file mode 100644 index 5eb60dd57dea2..0000000000000 --- a/packages/twenty-apps/public/twenty-meeting-bot/src/logic-functions/constants/recall-bot-automatic-leave.ts +++ /dev/null @@ -1,74 +0,0 @@ -import { isUndefined } from '@sniptt/guards'; - -import { MEETING_BOT_EVERYONE_LEFT_TIMEOUT_SECONDS_ENV_VAR_NAME } from 'src/logic-functions/constants/meeting-bot-everyone-left-timeout-seconds-env-var-name'; -import { MEETING_BOT_NOONE_JOINED_TIMEOUT_SECONDS_ENV_VAR_NAME } from 'src/logic-functions/constants/meeting-bot-noone-joined-timeout-seconds-env-var-name'; -import { MEETING_BOT_WAITING_ROOM_TIMEOUT_SECONDS_ENV_VAR_NAME } from 'src/logic-functions/constants/meeting-bot-waiting-room-timeout-seconds-env-var-name'; -import { RECALL_BOT_EVERYONE_LEFT_MIN_ACTIVATE_AFTER_SECONDS } from 'src/logic-functions/constants/recall-bot-everyone-left-min-activate-after-seconds'; -import { getApplicationVariableValue } from 'src/logic-functions/utils/get-application-variable-value.util'; -import { isNonEmptyString } from 'src/logic-functions/utils/is-non-empty-string.util'; - -type RecallBotAutomaticLeave = { - waiting_room_timeout?: number; - noone_joined_timeout?: number; - everyone_left_timeout?: { - timeout: number; - activate_after: number; - }; -}; - -export const getRecallBotAutomaticLeave = (): - | RecallBotAutomaticLeave - | undefined => { - const waitingRoomTimeoutSeconds = getOptionalPositiveIntegerVariable( - MEETING_BOT_WAITING_ROOM_TIMEOUT_SECONDS_ENV_VAR_NAME, - ); - const nooneJoinedTimeoutSeconds = getOptionalPositiveIntegerVariable( - MEETING_BOT_NOONE_JOINED_TIMEOUT_SECONDS_ENV_VAR_NAME, - ); - const everyoneLeftTimeoutSeconds = getOptionalPositiveIntegerVariable( - MEETING_BOT_EVERYONE_LEFT_TIMEOUT_SECONDS_ENV_VAR_NAME, - ); - - const automaticLeave: RecallBotAutomaticLeave = {}; - - if (!isUndefined(waitingRoomTimeoutSeconds)) { - automaticLeave.waiting_room_timeout = waitingRoomTimeoutSeconds; - } - - if (!isUndefined(nooneJoinedTimeoutSeconds)) { - automaticLeave.noone_joined_timeout = nooneJoinedTimeoutSeconds; - } - - if (!isUndefined(everyoneLeftTimeoutSeconds)) { - automaticLeave.everyone_left_timeout = { - timeout: everyoneLeftTimeoutSeconds, - activate_after: RECALL_BOT_EVERYONE_LEFT_MIN_ACTIVATE_AFTER_SECONDS, - }; - } - - return Object.keys(automaticLeave).length === 0 ? undefined : automaticLeave; -}; - -const getOptionalPositiveIntegerVariable = ( - variableName: string, -): number | undefined => { - const rawValue = normalizeOptionalString( - getApplicationVariableValue(variableName), - ); - - if (isUndefined(rawValue)) { - return undefined; - } - - const timeoutSeconds = Number(rawValue); - - if (!Number.isInteger(timeoutSeconds) || timeoutSeconds <= 0) { - return undefined; - } - - return timeoutSeconds; -}; - -const normalizeOptionalString = ( - value: string | undefined, -): string | undefined => (isNonEmptyString(value) ? value.trim() : undefined); diff --git a/packages/twenty-apps/public/twenty-meeting-bot/src/logic-functions/constants/recall-bot-everyone-left-min-activate-after-seconds.ts b/packages/twenty-apps/public/twenty-meeting-bot/src/logic-functions/constants/recall-bot-everyone-left-min-activate-after-seconds.ts deleted file mode 100644 index 1279213348ee4..0000000000000 --- a/packages/twenty-apps/public/twenty-meeting-bot/src/logic-functions/constants/recall-bot-everyone-left-min-activate-after-seconds.ts +++ /dev/null @@ -1 +0,0 @@ -export const RECALL_BOT_EVERYONE_LEFT_MIN_ACTIVATE_AFTER_SECONDS = 1; diff --git a/packages/twenty-apps/public/twenty-meeting-bot/src/logic-functions/constants/recall-bot-recording-config.ts b/packages/twenty-apps/public/twenty-meeting-bot/src/logic-functions/constants/recall-bot-recording-config.ts deleted file mode 100644 index 7c6655b581880..0000000000000 --- a/packages/twenty-apps/public/twenty-meeting-bot/src/logic-functions/constants/recall-bot-recording-config.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { DEFAULT_MEETING_BOT_RECORDING_RETENTION_HOURS } from 'src/logic-functions/constants/default-meeting-bot-recording-retention-hours'; -import { MEETING_BOT_RECORDING_RETENTION_HOURS_ENV_VAR_NAME } from 'src/logic-functions/constants/meeting-bot-recording-retention-hours-env-var-name'; -import { getApplicationVariableValue } from 'src/logic-functions/utils/get-application-variable-value.util'; -import { isNonEmptyString } from 'src/logic-functions/utils/is-non-empty-string.util'; - -type RecallBotRecordingConfig = { - video_mixed_mp4: Record; - audio_mixed_mp3: Record; - retention: { type: 'timed'; hours: number }; -}; - -// Recall only produces artifacts declared at bot creation; both gate COMPLETED. -export const getRecallBotRecordingConfig = (): RecallBotRecordingConfig => { - const configuredRecordingRetentionHours = getApplicationVariableValue( - MEETING_BOT_RECORDING_RETENTION_HOURS_ENV_VAR_NAME, - ); - - const recordingRetentionHours = isNonEmptyString( - configuredRecordingRetentionHours, - ) - ? Number(configuredRecordingRetentionHours.trim()) - : NaN; - - const resolvedRecordingRetentionHours = - Number.isInteger(recordingRetentionHours) && recordingRetentionHours > 0 - ? recordingRetentionHours - : DEFAULT_MEETING_BOT_RECORDING_RETENTION_HOURS; - - return { - video_mixed_mp4: {}, - audio_mixed_mp3: {}, - retention: { type: 'timed', hours: resolvedRecordingRetentionHours }, - }; -}; diff --git a/packages/twenty-apps/public/twenty-meeting-bot/src/logic-functions/constants/recall-region-env-var-name.ts b/packages/twenty-apps/public/twenty-meeting-bot/src/logic-functions/constants/recall-region-env-var-name.ts deleted file mode 100644 index 31c9a29523ed6..0000000000000 --- a/packages/twenty-apps/public/twenty-meeting-bot/src/logic-functions/constants/recall-region-env-var-name.ts +++ /dev/null @@ -1 +0,0 @@ -export const RECALL_REGION_ENV_VAR_NAME = 'RECALL_REGION'; diff --git a/packages/twenty-apps/public/twenty-meeting-bot/src/logic-functions/constants/recall-webhook-secret-env-var-name.ts b/packages/twenty-apps/public/twenty-meeting-bot/src/logic-functions/constants/recall-webhook-secret-env-var-name.ts deleted file mode 100644 index 520dfd2ff17dc..0000000000000 --- a/packages/twenty-apps/public/twenty-meeting-bot/src/logic-functions/constants/recall-webhook-secret-env-var-name.ts +++ /dev/null @@ -1 +0,0 @@ -export const RECALL_WEBHOOK_SECRET_ENV_VAR_NAME = 'RECALL_WEBHOOK_SECRET'; diff --git a/packages/twenty-apps/public/twenty-meeting-bot/src/logic-functions/constants/restricted-field-placeholder.ts b/packages/twenty-apps/public/twenty-meeting-bot/src/logic-functions/constants/restricted-field-placeholder.ts deleted file mode 100644 index 80388082c8ad3..0000000000000 --- a/packages/twenty-apps/public/twenty-meeting-bot/src/logic-functions/constants/restricted-field-placeholder.ts +++ /dev/null @@ -1,3 +0,0 @@ -// Mirrors twenty-shared; calendar restrictions write it over title/description. -export const RESTRICTED_FIELD_PLACEHOLDER = - 'FIELD_RESTRICTED_ADDITIONAL_PERMISSIONS_REQUIRED'; diff --git a/packages/twenty-apps/public/twenty-meeting-bot/src/logic-functions/constants/stale-bot-state-cron-pattern.ts b/packages/twenty-apps/public/twenty-meeting-bot/src/logic-functions/constants/stale-bot-state-cron-pattern.ts deleted file mode 100644 index a191b0af71ec6..0000000000000 --- a/packages/twenty-apps/public/twenty-meeting-bot/src/logic-functions/constants/stale-bot-state-cron-pattern.ts +++ /dev/null @@ -1 +0,0 @@ -export const STALE_BOT_STATE_CRON_PATTERN = '*/5 * * * *'; diff --git a/packages/twenty-apps/public/twenty-meeting-bot/src/logic-functions/constants/twenty-page-size.ts b/packages/twenty-apps/public/twenty-meeting-bot/src/logic-functions/constants/twenty-page-size.ts deleted file mode 100644 index 51637dc13dc82..0000000000000 --- a/packages/twenty-apps/public/twenty-meeting-bot/src/logic-functions/constants/twenty-page-size.ts +++ /dev/null @@ -1 +0,0 @@ -export const TWENTY_PAGE_SIZE = 100; diff --git a/packages/twenty-apps/public/twenty-meeting-bot/src/logic-functions/data/__tests__/complete-call-recording-ingestion.test.ts b/packages/twenty-apps/public/twenty-meeting-bot/src/logic-functions/data/__tests__/complete-call-recording-ingestion.test.ts deleted file mode 100644 index 820f7d3b3a735..0000000000000 --- a/packages/twenty-apps/public/twenty-meeting-bot/src/logic-functions/data/__tests__/complete-call-recording-ingestion.test.ts +++ /dev/null @@ -1,55 +0,0 @@ -import { describe, expect, it, vi } from 'vitest'; - -import { completeCallRecordingIngestion } from 'src/logic-functions/data/complete-call-recording-ingestion.util'; - -describe('completeCallRecordingIngestion', () => { - it('guards the flip with non-terminal statuses and returns true when the row is claimed', async () => { - let capturedArgs: { filter: unknown; data: unknown } | undefined; - const mutation = vi.fn(async (mutationArg: any) => { - capturedArgs = mutationArg.updateCallRecordings.__args; - - return { updateCallRecordings: [{ id: 'call-recording-1' }] }; - }); - - const claimed = await completeCallRecordingIngestion( - { mutation } as never, - { - id: 'call-recording-1', - }, - ); - - expect(claimed).toBe(true); - expect(mutation).toHaveBeenCalledTimes(1); - expect(capturedArgs?.filter).toEqual({ - id: { eq: 'call-recording-1' }, - status: { in: ['SCHEDULED', 'JOINING', 'RECORDING', 'PROCESSING'] }, - }); - expect(capturedArgs?.data).toEqual({ status: 'COMPLETED' }); - }); - - it('returns false when the row was already COMPLETED, so the loser cannot charge', async () => { - const mutation = vi.fn(async () => ({ updateCallRecordings: [] })); - - const claimed = await completeCallRecordingIngestion( - { mutation } as never, - { - id: 'call-recording-1', - }, - ); - - expect(claimed).toBe(false); - }); - - it('returns false when the API omits the result list', async () => { - const mutation = vi.fn(async () => ({})); - - const claimed = await completeCallRecordingIngestion( - { mutation } as never, - { - id: 'call-recording-1', - }, - ); - - expect(claimed).toBe(false); - }); -}); diff --git a/packages/twenty-apps/public/twenty-meeting-bot/src/logic-functions/data/__tests__/fetch-all-nodes.test.ts b/packages/twenty-apps/public/twenty-meeting-bot/src/logic-functions/data/__tests__/fetch-all-nodes.test.ts deleted file mode 100644 index c82576d67fec3..0000000000000 --- a/packages/twenty-apps/public/twenty-meeting-bot/src/logic-functions/data/__tests__/fetch-all-nodes.test.ts +++ /dev/null @@ -1,43 +0,0 @@ -import { describe, expect, it, vi } from 'vitest'; - -import { fetchAllNodes } from 'src/logic-functions/data/fetch-all-nodes.util'; - -describe('fetchAllNodes', () => { - it('collects nodes across pages until hasNextPage is false', async () => { - const fetchPage = vi - .fn() - .mockResolvedValueOnce({ - pageInfo: { hasNextPage: true, endCursor: 'cursor-1' }, - edges: [{ node: 'node-1' }, { node: 'node-2' }], - }) - .mockResolvedValueOnce({ - pageInfo: { hasNextPage: false, endCursor: 'cursor-2' }, - edges: [{ node: 'node-3' }], - }); - - const nodes = await fetchAllNodes(fetchPage); - - expect(nodes).toEqual(['node-1', 'node-2', 'node-3']); - expect(fetchPage).toHaveBeenNthCalledWith(1, undefined); - expect(fetchPage).toHaveBeenNthCalledWith(2, 'cursor-1'); - }); - - it('throws when hasNextPage is true without an endCursor', async () => { - const fetchPage = vi.fn().mockResolvedValue({ - pageInfo: { hasNextPage: true, endCursor: null }, - edges: [{ node: 'node-1' }], - }); - - await expect(fetchAllNodes(fetchPage)).rejects.toThrow( - 'Inconsistent pagination state: hasNextPage is true without an endCursor', - ); - }); - - it('throws when the query returns no connection', async () => { - const fetchPage = vi.fn().mockResolvedValue(undefined); - - await expect(fetchAllNodes(fetchPage)).rejects.toThrow( - 'Pagination query returned no connection', - ); - }); -}); diff --git a/packages/twenty-apps/public/twenty-meeting-bot/src/logic-functions/data/__tests__/get-current-workspace-id.test.ts b/packages/twenty-apps/public/twenty-meeting-bot/src/logic-functions/data/__tests__/get-current-workspace-id.test.ts deleted file mode 100644 index 4e061d9c29ec7..0000000000000 --- a/packages/twenty-apps/public/twenty-meeting-bot/src/logic-functions/data/__tests__/get-current-workspace-id.test.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { afterEach, describe, expect, it } from 'vitest'; - -import { getCurrentWorkspaceId } from 'src/logic-functions/data/get-current-workspace-id.util'; - -const APP_ACCESS_TOKEN_ENV_VAR_NAME = 'TWENTY_APP_ACCESS_TOKEN'; -const ORIGINAL_APP_ACCESS_TOKEN = - process.env[APP_ACCESS_TOKEN_ENV_VAR_NAME]; -const WORKSPACE_ID = '123e4567-e89b-12d3-a456-426614174000'; - -const restoreOriginalAppAccessToken = () => { - if (ORIGINAL_APP_ACCESS_TOKEN === undefined) { - delete process.env[APP_ACCESS_TOKEN_ENV_VAR_NAME]; - - return; - } - - process.env[APP_ACCESS_TOKEN_ENV_VAR_NAME] = ORIGINAL_APP_ACCESS_TOKEN; -}; - -const buildAccessToken = (payload: Record): string => - [ - Buffer.from(JSON.stringify({ alg: 'none' })).toString('base64url'), - Buffer.from(JSON.stringify(payload)).toString('base64url'), - 'signature', - ].join('.'); - -describe('getCurrentWorkspaceId', () => { - afterEach(() => { - restoreOriginalAppAccessToken(); - }); - - it('reads the workspace id from the app access token payload', () => { - process.env[APP_ACCESS_TOKEN_ENV_VAR_NAME] = buildAccessToken({ - workspaceId: WORKSPACE_ID, - }); - - expect(getCurrentWorkspaceId()).toBe(WORKSPACE_ID); - }); -}); diff --git a/packages/twenty-apps/public/twenty-meeting-bot/src/logic-functions/data/__tests__/strip-restricted-field-value.test.ts b/packages/twenty-apps/public/twenty-meeting-bot/src/logic-functions/data/__tests__/strip-restricted-field-value.test.ts deleted file mode 100644 index 27bc08f2f9a4c..0000000000000 --- a/packages/twenty-apps/public/twenty-meeting-bot/src/logic-functions/data/__tests__/strip-restricted-field-value.test.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { describe, expect, it } from 'vitest'; - -import { RESTRICTED_FIELD_PLACEHOLDER } from 'src/logic-functions/constants/restricted-field-placeholder'; -import { stripRestrictedFieldValue } from 'src/logic-functions/data/strip-restricted-field-value.util'; - -describe('stripRestrictedFieldValue', () => { - it('drops the calendar visibility restriction placeholder', () => { - expect( - stripRestrictedFieldValue(RESTRICTED_FIELD_PLACEHOLDER), - ).toBeUndefined(); - }); - - it('keeps regular values', () => { - expect(stripRestrictedFieldValue('Customer Discovery Call')).toBe( - 'Customer Discovery Call', - ); - }); - - it('keeps undefined', () => { - expect(stripRestrictedFieldValue(undefined)).toBeUndefined(); - }); -}); diff --git a/packages/twenty-apps/public/twenty-meeting-bot/src/logic-functions/data/complete-call-recording-ingestion.util.ts b/packages/twenty-apps/public/twenty-meeting-bot/src/logic-functions/data/complete-call-recording-ingestion.util.ts deleted file mode 100644 index b1f2dbc5668ab..0000000000000 --- a/packages/twenty-apps/public/twenty-meeting-bot/src/logic-functions/data/complete-call-recording-ingestion.util.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { type CoreApiClient } from 'twenty-client-sdk/core'; - -import { CallRecordingStatus } from 'src/logic-functions/constants/call-recording-status'; -import { NON_TERMINAL_CALL_RECORDING_STATUSES } from 'src/logic-functions/constants/non-terminal-call-recording-statuses'; -import { - executeCurrentSchemaMutation, - type CurrentSchemaUpdateCallRecordingsMutation, -} from 'src/logic-functions/data/execute-current-schema-mutation.util'; - -export const completeCallRecordingIngestion = async ( - client: CoreApiClient, - { id }: { id: string }, -): Promise => { - const mutation = { - updateCallRecordings: { - __args: { - filter: { - id: { eq: id }, - status: { in: NON_TERMINAL_CALL_RECORDING_STATUSES }, - }, - data: { status: CallRecordingStatus.COMPLETED }, - }, - id: true, - }, - } satisfies CurrentSchemaUpdateCallRecordingsMutation; - - const result = await executeCurrentSchemaMutation(client, mutation); - - return (result.updateCallRecordings ?? []).length > 0; -}; diff --git a/packages/twenty-apps/public/twenty-meeting-bot/src/logic-functions/data/create-call-recording.util.ts b/packages/twenty-apps/public/twenty-meeting-bot/src/logic-functions/data/create-call-recording.util.ts deleted file mode 100644 index c8ed9a95f5b25..0000000000000 --- a/packages/twenty-apps/public/twenty-meeting-bot/src/logic-functions/data/create-call-recording.util.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { isUndefined } from '@sniptt/guards'; -import { type CoreApiClient } from 'twenty-client-sdk/core'; - -import { type CallRecordingRequestStatus } from 'src/logic-functions/constants/call-recording-request-status'; -import { type CallRecordingStatus } from 'src/logic-functions/constants/call-recording-status'; - -export type ScheduledCallRecordingFields = { - title: string | null; - status: CallRecordingStatus.SCHEDULED; - recordingRequestStatus: CallRecordingRequestStatus.REQUESTED; - calendarEventId: string; -}; - -export const createCallRecording = async ( - client: CoreApiClient, - { - id, - data, - }: { - id: string; - data: ScheduledCallRecordingFields; - }, -): Promise => { - const mutationResult = await client.mutation({ - createCallRecording: { - __args: { - data: { id, ...data }, - }, - id: true, - }, - }); - const createdCallRecordingId = mutationResult.createCallRecording?.id; - - if (isUndefined(createdCallRecordingId)) { - throw new Error( - 'createCallRecording mutation did not return a call recording id', - ); - } - - return createdCallRecordingId; -}; diff --git a/packages/twenty-apps/public/twenty-meeting-bot/src/logic-functions/data/execute-current-schema-mutation.util.ts b/packages/twenty-apps/public/twenty-meeting-bot/src/logic-functions/data/execute-current-schema-mutation.util.ts deleted file mode 100644 index 8e5dfcc03b590..0000000000000 --- a/packages/twenty-apps/public/twenty-meeting-bot/src/logic-functions/data/execute-current-schema-mutation.util.ts +++ /dev/null @@ -1,54 +0,0 @@ -import { type CoreApiClient } from 'twenty-client-sdk/core'; - -import { type CallRecordingStatus } from 'src/logic-functions/constants/call-recording-status'; -import { type CallRecordingUpdateFields } from 'src/logic-functions/data/update-call-recording.util'; - -type CurrentSchemaMutationFunction = ( - mutation: CurrentSchemaMutation, -) => Promise; - -export type CurrentSchemaUpdateCallRecordingMutation = { - updateCallRecording: { - __args: { - id: string; - data: CallRecordingUpdateFields; - }; - id?: true; - status?: true; - }; -}; - -export type CurrentSchemaUpdateCallRecordingsMutation = { - updateCallRecordings: { - __args: { - filter: { - id: { eq: string }; - status?: { in: CallRecordingStatus[] }; - }; - data: Pick; - }; - id?: true; - }; -}; - -type CurrentSchemaMutation = - | CurrentSchemaUpdateCallRecordingMutation - | CurrentSchemaUpdateCallRecordingsMutation; - -type CurrentSchemaMutationResult = { - updateCallRecording?: { id?: string; status?: string | null } | null; - updateCallRecordings?: { id?: string }[] | null; -}; - -// TODO: Remove this bridge once the released SDK includes the current -// CallRecording schema with FAILED and meetingBotFailureReason. -export const executeCurrentSchemaMutation = ( - client: CoreApiClient, - mutation: CurrentSchemaMutation, -): Promise => { - const currentSchemaClient = client as { - mutation: CurrentSchemaMutationFunction; - }; - - return currentSchemaClient.mutation(mutation); -}; diff --git a/packages/twenty-apps/public/twenty-meeting-bot/src/logic-functions/data/fetch-all-nodes.util.ts b/packages/twenty-apps/public/twenty-meeting-bot/src/logic-functions/data/fetch-all-nodes.util.ts deleted file mode 100644 index ce16ce4fe802d..0000000000000 --- a/packages/twenty-apps/public/twenty-meeting-bot/src/logic-functions/data/fetch-all-nodes.util.ts +++ /dev/null @@ -1,44 +0,0 @@ -import { isString, isUndefined } from '@sniptt/guards'; - -export type ConnectionPage = { - pageInfo?: { - hasNextPage?: boolean | null; - endCursor?: string | null; - } | null; - edges?: Array<{ node: TNode }> | null; -}; - -export const fetchAllNodes = async ( - fetchPage: ( - afterCursor: string | undefined, - ) => Promise | undefined>, -): Promise => { - const nodes: TNode[] = []; - let hasNextPage = true; - let afterCursor: string | undefined; - - while (hasNextPage) { - const connection = await fetchPage(afterCursor); - - if (isUndefined(connection)) { - throw new Error('Pagination query returned no connection'); - } - - for (const edge of connection.edges ?? []) { - nodes.push(edge.node); - } - - hasNextPage = connection.pageInfo?.hasNextPage === true; - const endCursor = connection.pageInfo?.endCursor; - - if (hasNextPage && !isString(endCursor)) { - throw new Error( - 'Inconsistent pagination state: hasNextPage is true without an endCursor', - ); - } - - afterCursor = isString(endCursor) ? endCursor : undefined; - } - - return nodes; -}; diff --git a/packages/twenty-apps/public/twenty-meeting-bot/src/logic-functions/data/fetch-calendar-events-by-filter.util.ts b/packages/twenty-apps/public/twenty-meeting-bot/src/logic-functions/data/fetch-calendar-events-by-filter.util.ts deleted file mode 100644 index f7d6b7ec8f879..0000000000000 --- a/packages/twenty-apps/public/twenty-meeting-bot/src/logic-functions/data/fetch-calendar-events-by-filter.util.ts +++ /dev/null @@ -1,80 +0,0 @@ -import { isString, isUndefined } from '@sniptt/guards'; -import { type CoreApiClient } from 'twenty-client-sdk/core'; - -import { TWENTY_PAGE_SIZE } from 'src/logic-functions/constants/twenty-page-size'; -import { type CalendarEventRecord } from 'src/logic-functions/types/calendar-event-record.type'; -import { - fetchAllNodes, - type ConnectionPage, -} from 'src/logic-functions/data/fetch-all-nodes.util'; -import { isNonEmptyString } from 'src/logic-functions/utils/is-non-empty-string.util'; -import { stripRestrictedFieldValue } from 'src/logic-functions/data/strip-restricted-field-value.util'; - -type CalendarEventNode = { - id: string; - title?: string | null; - isCanceled?: boolean | null; - startsAt?: string | null; - endsAt?: string | null; - iCalUid?: string | null; - conferenceLink?: { primaryLinkUrl?: string | null } | null; - meetingBotPreference?: string | null; -}; - -export const fetchCalendarEventsByFilter = async ( - client: CoreApiClient, - filter: Record, -): Promise => { - const calendarEventNodes = await fetchAllNodes( - async (afterCursor) => { - const queryResult = await client.query({ - calendarEvents: { - __args: { - filter, - first: TWENTY_PAGE_SIZE, - ...(isUndefined(afterCursor) ? {} : { after: afterCursor }), - }, - pageInfo: { - hasNextPage: true, - endCursor: true, - }, - edges: { - node: { - id: true, - title: true, - isCanceled: true, - startsAt: true, - endsAt: true, - iCalUid: true, - conferenceLink: { - primaryLinkUrl: true, - }, - meetingBotPreference: true, - }, - }, - }, - }); - - return queryResult.calendarEvents as - | ConnectionPage - | undefined; - }, - ); - - return calendarEventNodes.map((calendarEvent) => ({ - id: calendarEvent.id, - title: stripRestrictedFieldValue(calendarEvent.title ?? undefined), - isCanceled: calendarEvent.isCanceled ?? false, - startsAt: calendarEvent.startsAt ?? undefined, - endsAt: calendarEvent.endsAt ?? undefined, - iCalUid: calendarEvent.iCalUid ?? undefined, - conferenceLinkUrl: isNonEmptyString( - calendarEvent.conferenceLink?.primaryLinkUrl, - ) - ? calendarEvent.conferenceLink.primaryLinkUrl - : undefined, - meetingBotPreference: isString(calendarEvent.meetingBotPreference) - ? calendarEvent.meetingBotPreference - : undefined, - })); -}; diff --git a/packages/twenty-apps/public/twenty-meeting-bot/src/logic-functions/data/fetch-calendar-events-by-ids.util.ts b/packages/twenty-apps/public/twenty-meeting-bot/src/logic-functions/data/fetch-calendar-events-by-ids.util.ts deleted file mode 100644 index d703198a5f9c2..0000000000000 --- a/packages/twenty-apps/public/twenty-meeting-bot/src/logic-functions/data/fetch-calendar-events-by-ids.util.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { type CoreApiClient } from 'twenty-client-sdk/core'; - -import { type CalendarEventRecord } from 'src/logic-functions/types/calendar-event-record.type'; -import { fetchCalendarEventsByFilter } from 'src/logic-functions/data/fetch-calendar-events-by-filter.util'; -import { getUniqueSortedIds } from 'src/logic-functions/utils/get-unique-sorted-ids.util'; - -export const fetchCalendarEventsByIds = async ( - client: CoreApiClient, - calendarEventIds: string[], -): Promise => { - const uniqueCalendarEventIds = getUniqueSortedIds(calendarEventIds); - - if (uniqueCalendarEventIds.length === 0) { - return []; - } - - return fetchCalendarEventsByFilter(client, { - id: { in: uniqueCalendarEventIds }, - }); -}; diff --git a/packages/twenty-apps/public/twenty-meeting-bot/src/logic-functions/data/fetch-calendar-events-by-starts-at-values.util.ts b/packages/twenty-apps/public/twenty-meeting-bot/src/logic-functions/data/fetch-calendar-events-by-starts-at-values.util.ts deleted file mode 100644 index aa5609cf8c084..0000000000000 --- a/packages/twenty-apps/public/twenty-meeting-bot/src/logic-functions/data/fetch-calendar-events-by-starts-at-values.util.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { type CoreApiClient } from 'twenty-client-sdk/core'; - -import { type CalendarEventRecord } from 'src/logic-functions/types/calendar-event-record.type'; -import { fetchCalendarEventsByFilter } from 'src/logic-functions/data/fetch-calendar-events-by-filter.util'; - -export const fetchCalendarEventsByStartsAtValues = async ( - client: CoreApiClient, - startsAtValues: string[], -): Promise => { - const uniqueStartsAtValues = [...new Set(startsAtValues)].sort(); - - if (uniqueStartsAtValues.length === 0) { - return []; - } - - return fetchCalendarEventsByFilter(client, { - startsAt: { in: uniqueStartsAtValues }, - }); -}; diff --git a/packages/twenty-apps/public/twenty-meeting-bot/src/logic-functions/data/find-call-recordings-by-calendar-event-ids.util.ts b/packages/twenty-apps/public/twenty-meeting-bot/src/logic-functions/data/find-call-recordings-by-calendar-event-ids.util.ts deleted file mode 100644 index b07ed738e78ae..0000000000000 --- a/packages/twenty-apps/public/twenty-meeting-bot/src/logic-functions/data/find-call-recordings-by-calendar-event-ids.util.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { type CoreApiClient } from 'twenty-client-sdk/core'; - -import { type CallRecordingRecord } from 'src/logic-functions/types/call-recording-record.type'; -import { findCallRecordingsByFilter } from 'src/logic-functions/data/find-call-recordings-by-filter.util'; - -export const findCallRecordingsByCalendarEventIds = async ( - client: CoreApiClient, - calendarEventIds: string[], -): Promise => { - if (calendarEventIds.length === 0) { - return []; - } - - return findCallRecordingsByFilter(client, { - calendarEventId: { in: calendarEventIds }, - }); -}; diff --git a/packages/twenty-apps/public/twenty-meeting-bot/src/logic-functions/data/find-call-recordings-by-filter.util.ts b/packages/twenty-apps/public/twenty-meeting-bot/src/logic-functions/data/find-call-recordings-by-filter.util.ts deleted file mode 100644 index fd2de1ab968de..0000000000000 --- a/packages/twenty-apps/public/twenty-meeting-bot/src/logic-functions/data/find-call-recordings-by-filter.util.ts +++ /dev/null @@ -1,102 +0,0 @@ -import { isUndefined } from '@sniptt/guards'; -import { type CoreApiClient } from 'twenty-client-sdk/core'; - -import { CallRecordingRequestStatus } from 'src/logic-functions/constants/call-recording-request-status'; -import { TWENTY_PAGE_SIZE } from 'src/logic-functions/constants/twenty-page-size'; -import { type CallRecordingRecord } from 'src/logic-functions/types/call-recording-record.type'; -import { - fetchAllNodes, - type ConnectionPage, -} from 'src/logic-functions/data/fetch-all-nodes.util'; -import { isNonEmptyString } from 'src/logic-functions/utils/is-non-empty-string.util'; - -type CallRecordingNode = { - id: string; - title?: string | null; - status?: string | null; - recordingRequestStatus?: unknown; - startedAt?: string | null; - endedAt?: string | null; - calendarEventId?: string | null; - externalBotId?: string | null; - externalRecordingId?: string | null; - meetingBotFailureReason?: string | null; -}; - -export const findCallRecordingsByFilter = async ( - client: CoreApiClient, - filter: Record, -): Promise => { - const callRecordingNodes = await fetchAllNodes( - async (afterCursor) => { - const queryResult = await client.query({ - callRecordings: { - __args: { - filter, - first: TWENTY_PAGE_SIZE, - ...(isUndefined(afterCursor) ? {} : { after: afterCursor }), - }, - pageInfo: { - hasNextPage: true, - endCursor: true, - }, - edges: { - node: { - id: true, - title: true, - status: true, - recordingRequestStatus: true, - startedAt: true, - endedAt: true, - calendarEventId: true, - externalBotId: true, - externalRecordingId: true, - meetingBotFailureReason: true, - }, - }, - }, - }); - - return queryResult.callRecordings as - | ConnectionPage - | undefined; - }, - ); - - return callRecordingNodes.map((callRecording) => ({ - id: callRecording.id, - title: callRecording.title ?? undefined, - status: callRecording.status ?? undefined, - recordingRequestStatus: normalizeCallRecordingRequestStatus( - callRecording.recordingRequestStatus, - ), - startedAt: callRecording.startedAt ?? undefined, - endedAt: callRecording.endedAt ?? undefined, - calendarEventId: callRecording.calendarEventId ?? undefined, - externalBotId: normalizeOptionalString(callRecording.externalBotId), - externalRecordingId: normalizeOptionalString( - callRecording.externalRecordingId, - ), - meetingBotFailureReason: normalizeOptionalString( - callRecording.meetingBotFailureReason, - ), - })); -}; - -const normalizeOptionalString = ( - value: string | null | undefined, -): string | undefined => (isNonEmptyString(value) ? value : undefined); - -const normalizeCallRecordingRequestStatus = ( - recordingRequestStatus: unknown, -): CallRecordingRequestStatus | undefined => { - if (recordingRequestStatus === CallRecordingRequestStatus.REQUESTED) { - return recordingRequestStatus; - } - - if (recordingRequestStatus === CallRecordingRequestStatus.CANCELED) { - return recordingRequestStatus; - } - - return undefined; -}; diff --git a/packages/twenty-apps/public/twenty-meeting-bot/src/logic-functions/data/find-call-recordings-by-ids.util.ts b/packages/twenty-apps/public/twenty-meeting-bot/src/logic-functions/data/find-call-recordings-by-ids.util.ts deleted file mode 100644 index 5ef2493faf9a2..0000000000000 --- a/packages/twenty-apps/public/twenty-meeting-bot/src/logic-functions/data/find-call-recordings-by-ids.util.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { type CoreApiClient } from 'twenty-client-sdk/core'; - -import { type CallRecordingRecord } from 'src/logic-functions/types/call-recording-record.type'; -import { findCallRecordingsByFilter } from 'src/logic-functions/data/find-call-recordings-by-filter.util'; - -export const findCallRecordingsByIds = async ( - client: CoreApiClient, - callRecordingIds: string[], -): Promise => { - if (callRecordingIds.length === 0) { - return []; - } - - return findCallRecordingsByFilter(client, { - id: { in: callRecordingIds }, - }); -}; diff --git a/packages/twenty-apps/public/twenty-meeting-bot/src/logic-functions/data/find-open-scheduled-call-recordings.util.ts b/packages/twenty-apps/public/twenty-meeting-bot/src/logic-functions/data/find-open-scheduled-call-recordings.util.ts deleted file mode 100644 index 5bd855c6f4449..0000000000000 --- a/packages/twenty-apps/public/twenty-meeting-bot/src/logic-functions/data/find-open-scheduled-call-recordings.util.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { type CoreApiClient } from 'twenty-client-sdk/core'; - -import { CallRecordingRequestStatus } from 'src/logic-functions/constants/call-recording-request-status'; -import { CallRecordingStatus } from 'src/logic-functions/constants/call-recording-status'; -import { type CallRecordingRecord } from 'src/logic-functions/types/call-recording-record.type'; -import { findCallRecordingsByFilter } from 'src/logic-functions/data/find-call-recordings-by-filter.util'; - -export const findOpenScheduledCallRecordings = async ( - client: CoreApiClient, -): Promise => - findCallRecordingsByFilter(client, { - recordingRequestStatus: { eq: CallRecordingRequestStatus.REQUESTED }, - status: { eq: CallRecordingStatus.SCHEDULED }, - }); diff --git a/packages/twenty-apps/public/twenty-meeting-bot/src/logic-functions/data/get-current-workspace-id.util.ts b/packages/twenty-apps/public/twenty-meeting-bot/src/logic-functions/data/get-current-workspace-id.util.ts deleted file mode 100644 index 4f0c8922006c2..0000000000000 --- a/packages/twenty-apps/public/twenty-meeting-bot/src/logic-functions/data/get-current-workspace-id.util.ts +++ /dev/null @@ -1,36 +0,0 @@ -import { isUndefined } from '@sniptt/guards'; - -import { asRecord } from 'src/logic-functions/utils/as-record.util'; -import { getString } from 'src/logic-functions/utils/get-string.util'; - -const APP_ACCESS_TOKEN_ENV_VAR_NAME = 'TWENTY_APP_ACCESS_TOKEN'; - -export const getCurrentWorkspaceId = (): string | undefined => { - const accessToken = getString(process.env[APP_ACCESS_TOKEN_ENV_VAR_NAME]); - - if (isUndefined(accessToken)) { - return undefined; - } - - return getWorkspaceIdFromAccessToken(accessToken); -}; - -const getWorkspaceIdFromAccessToken = ( - accessToken: string, -): string | undefined => { - const encodedPayload = accessToken.split('.')[1]; - - if (isUndefined(encodedPayload)) { - return undefined; - } - - try { - const payload = asRecord( - JSON.parse(Buffer.from(encodedPayload, 'base64url').toString('utf8')), - ); - - return getString(payload?.workspaceId); - } catch { - return undefined; - } -}; diff --git a/packages/twenty-apps/public/twenty-meeting-bot/src/logic-functions/data/strip-restricted-field-value.util.ts b/packages/twenty-apps/public/twenty-meeting-bot/src/logic-functions/data/strip-restricted-field-value.util.ts deleted file mode 100644 index 06b4a79a479fc..0000000000000 --- a/packages/twenty-apps/public/twenty-meeting-bot/src/logic-functions/data/strip-restricted-field-value.util.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { RESTRICTED_FIELD_PLACEHOLDER } from 'src/logic-functions/constants/restricted-field-placeholder'; - -export const stripRestrictedFieldValue = ( - value: string | undefined, -): string | undefined => - value === RESTRICTED_FIELD_PLACEHOLDER ? undefined : value; diff --git a/packages/twenty-apps/public/twenty-meeting-bot/src/logic-functions/data/update-call-recording.util.ts b/packages/twenty-apps/public/twenty-meeting-bot/src/logic-functions/data/update-call-recording.util.ts deleted file mode 100644 index 251e325702c6b..0000000000000 --- a/packages/twenty-apps/public/twenty-meeting-bot/src/logic-functions/data/update-call-recording.util.ts +++ /dev/null @@ -1,48 +0,0 @@ -import { type CoreApiClient } from 'twenty-client-sdk/core'; - -import { type CallRecordingRequestStatus } from 'src/logic-functions/constants/call-recording-request-status'; -import { type CallRecordingStatus } from 'src/logic-functions/constants/call-recording-status'; -import { - executeCurrentSchemaMutation, - type CurrentSchemaUpdateCallRecordingMutation, -} from 'src/logic-functions/data/execute-current-schema-mutation.util'; - -export type CallRecordingUpdateFields = Partial<{ - // null clears a previously synced title when the calendar title disappears. - title: string | null; - status: CallRecordingStatus; - recordingRequestStatus: CallRecordingRequestStatus; - startedAt: string; - endedAt: string; - calendarEventId: string; - // null clears stale app-owned state on cancel/eject or reschedule. - externalBotId: string | null; - externalRecordingId: string; - meetingBotFailureReason: string | null; - transcript: Record; - audio: { fileId: string; label: string }[]; - video: { fileId: string; label: string }[]; -}>; - -export const updateCallRecording = async ( - client: CoreApiClient, - { - id, - data, - }: { - id: string; - data: CallRecordingUpdateFields; - }, -): Promise => { - const mutation = { - updateCallRecording: { - __args: { - id, - data, - }, - id: true, - }, - } satisfies CurrentSchemaUpdateCallRecordingMutation; - - await executeCurrentSchemaMutation(client, mutation); -}; diff --git a/packages/twenty-apps/public/twenty-meeting-bot/src/logic-functions/domain/__tests__/build-meeting-bot-policy-result.test.ts b/packages/twenty-apps/public/twenty-meeting-bot/src/logic-functions/domain/__tests__/build-meeting-bot-policy-result.test.ts deleted file mode 100644 index 919b8d794831e..0000000000000 --- a/packages/twenty-apps/public/twenty-meeting-bot/src/logic-functions/domain/__tests__/build-meeting-bot-policy-result.test.ts +++ /dev/null @@ -1,47 +0,0 @@ -import { describe, expect, it } from 'vitest'; - -import { buildMeetingBotPolicyResult } from 'src/logic-functions/domain/build-meeting-bot-policy-result.util'; -import { type MeetingBotPolicyCalendarEventInput } from 'src/logic-functions/types/meeting-bot-policy-calendar-event-input.type'; - -const NOW = new Date('2026-01-01T12:00:00.000Z'); - -const buildCalendarEventInput = ( - overrides: Partial, -): MeetingBotPolicyCalendarEventInput => ({ - id: 'calendar-event-1', - isCanceled: false, - startsAt: '2026-01-01T13:00:00.000Z', - endsAt: '2026-01-01T14:00:00.000Z', - iCalUid: 'ical-uid-1', - conferenceLinkUrl: 'https://meet.example.com/customer-sync', - meetingBotPreference: undefined, - ...overrides, -}); - -describe('buildMeetingBotPolicyResult', () => { - it('requests a bot for the ON wire value', () => { - const policyResult = buildMeetingBotPolicyResult( - buildCalendarEventInput({ - meetingBotPreference: 'ON', - }), - NOW, - ); - - expect(policyResult.meetingBotPreference).toBe('ON'); - expect(policyResult.shouldRequestBot).toBe(true); - expect(policyResult.reason).toBe('RECORDING_ENABLED'); - }); - - it('does not request a bot for the OFF wire value', () => { - const policyResult = buildMeetingBotPolicyResult( - buildCalendarEventInput({ - meetingBotPreference: 'OFF', - }), - NOW, - ); - - expect(policyResult.meetingBotPreference).toBe('OFF'); - expect(policyResult.shouldRequestBot).toBe(false); - expect(policyResult.reason).toBe('PREFERENCE_OFF'); - }); -}); diff --git a/packages/twenty-apps/public/twenty-meeting-bot/src/logic-functions/domain/__tests__/compute-call-recording-charge.test.ts b/packages/twenty-apps/public/twenty-meeting-bot/src/logic-functions/domain/__tests__/compute-call-recording-charge.test.ts deleted file mode 100644 index 7abf925f9e531..0000000000000 --- a/packages/twenty-apps/public/twenty-meeting-bot/src/logic-functions/domain/__tests__/compute-call-recording-charge.test.ts +++ /dev/null @@ -1,71 +0,0 @@ -import { describe, expect, it } from 'vitest'; - -import { computeCallRecordingCharge } from 'src/logic-functions/domain/compute-call-recording-charge.util'; - -describe('computeCallRecordingCharge', () => { - it('charges one credit for a one-hour recording', () => { - expect( - computeCallRecordingCharge({ - startedAt: '2026-06-10T09:00:00.000Z', - endedAt: '2026-06-10T10:00:00.000Z', - }), - ).toEqual({ - creditsUsedMicro: 1_000_000, - quantityMinutes: 60, - }); - }); - - it('prorates partial hours by duration', () => { - expect( - computeCallRecordingCharge({ - startedAt: '2026-06-10T09:00:00.000Z', - endedAt: '2026-06-10T09:45:00.000Z', - }), - ).toEqual({ - creditsUsedMicro: 750_000, - quantityMinutes: 45, - }); - }); - - it('reports at least one minute for very short recordings', () => { - expect( - computeCallRecordingCharge({ - startedAt: '2026-06-10T09:00:00.000Z', - endedAt: '2026-06-10T09:00:30.000Z', - }), - ).toEqual({ - creditsUsedMicro: 8_333, - quantityMinutes: 1, - }); - }); - - it('returns undefined when either timestamp is missing', () => { - expect( - computeCallRecordingCharge({ - startedAt: undefined, - endedAt: '2026-06-10T10:00:00.000Z', - }), - ).toBeUndefined(); - expect( - computeCallRecordingCharge({ - startedAt: '2026-06-10T09:00:00.000Z', - endedAt: undefined, - }), - ).toBeUndefined(); - }); - - it('returns undefined for non-positive or unparseable durations', () => { - expect( - computeCallRecordingCharge({ - startedAt: '2026-06-10T10:00:00.000Z', - endedAt: '2026-06-10T09:00:00.000Z', - }), - ).toBeUndefined(); - expect( - computeCallRecordingCharge({ - startedAt: 'not-a-date', - endedAt: '2026-06-10T10:00:00.000Z', - }), - ).toBeUndefined(); - }); -}); diff --git a/packages/twenty-apps/public/twenty-meeting-bot/src/logic-functions/domain/__tests__/compute-call-recording-id-for-meeting.test.ts b/packages/twenty-apps/public/twenty-meeting-bot/src/logic-functions/domain/__tests__/compute-call-recording-id-for-meeting.test.ts deleted file mode 100644 index 45fea40e4bfe4..0000000000000 --- a/packages/twenty-apps/public/twenty-meeting-bot/src/logic-functions/domain/__tests__/compute-call-recording-id-for-meeting.test.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { describe, expect, it } from 'vitest'; - -import { computeCallRecordingIdForMeeting } from 'src/logic-functions/domain/compute-call-recording-id-for-meeting.util'; - -const UUID_V4_PATTERN = - /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/; - -describe('computeCallRecordingIdForMeeting', () => { - it('returns the same id for the same real meeting key', () => { - const realMeetingKey = - 'link:meet.example.com/sync:2026-01-01T13:00:00.000Z'; - - expect(computeCallRecordingIdForMeeting(realMeetingKey)).toBe( - computeCallRecordingIdForMeeting(realMeetingKey), - ); - }); - - it('returns different ids for different real meeting keys', () => { - expect( - computeCallRecordingIdForMeeting( - 'link:meet.example.com/sync:2026-01-01T13:00:00.000Z', - ), - ).not.toBe( - computeCallRecordingIdForMeeting( - 'link:meet.example.com/sync:2026-01-02T13:00:00.000Z', - ), - ); - }); - - it('returns a v4-shaped uuid', () => { - expect( - computeCallRecordingIdForMeeting( - 'ical:some-uid:2026-01-01T13:00:00.000Z', - ), - ).toMatch(UUID_V4_PATTERN); - }); -}); diff --git a/packages/twenty-apps/public/twenty-meeting-bot/src/logic-functions/domain/__tests__/compute-real-meeting-key.test.ts b/packages/twenty-apps/public/twenty-meeting-bot/src/logic-functions/domain/__tests__/compute-real-meeting-key.test.ts deleted file mode 100644 index 940316139e538..0000000000000 --- a/packages/twenty-apps/public/twenty-meeting-bot/src/logic-functions/domain/__tests__/compute-real-meeting-key.test.ts +++ /dev/null @@ -1,88 +0,0 @@ -import { describe, expect, it } from 'vitest'; - -import { computeRealMeetingKey } from 'src/logic-functions/domain/compute-real-meeting-key.util'; - -const STARTS_AT = '2026-01-01T13:00:00.000Z'; - -const buildInput = ( - overrides: Partial[0]> = {}, -) => ({ - calendarEventId: 'calendar-event-1', - conferenceLinkUrl: 'https://meet.example.com/customer-sync', - iCalUid: 'calendar-event-uid', - startsAt: STARTS_AT, - ...overrides, -}); - -describe('computeRealMeetingKey', () => { - it.each([ - [ - 'strips protocol, query, and fragment', - 'https://zoom.us/j/123?pwd=abc#section', - `link:zoom.us/j/123:${STARTS_AT}`, - ], - [ - 'strips www and lowercases', - 'HTTPS://WWW.Meet.Example.com/Customer-Sync', - `link:meet.example.com/customer-sync:${STARTS_AT}`, - ], - [ - 'strips trailing slashes', - 'https://meet.example.com/customer-sync///', - `link:meet.example.com/customer-sync:${STARTS_AT}`, - ], - [ - 'supports plain http links', - 'http://meet.example.com/customer-sync', - `link:meet.example.com/customer-sync:${STARTS_AT}`, - ], - ])('%s', (_label, conferenceLinkUrl, expectedKey) => { - expect(computeRealMeetingKey(buildInput({ conferenceLinkUrl }))).toBe( - expectedKey, - ); - }); - - it('produces the same key for the same meeting synced from two calendars', () => { - const fromFirstAttendee = computeRealMeetingKey( - buildInput({ - calendarEventId: 'calendar-event-1', - conferenceLinkUrl: 'https://zoom.us/j/123?pwd=first-attendee-token', - }), - ); - const fromSecondAttendee = computeRealMeetingKey( - buildInput({ - calendarEventId: 'calendar-event-2', - conferenceLinkUrl: - 'https://www.zoom.us/j/123?pwd=second-attendee-token', - }), - ); - - expect(fromFirstAttendee).toBe(fromSecondAttendee); - }); - - it('falls back to the iCal uid when the link is blank', () => { - expect( - computeRealMeetingKey(buildInput({ conferenceLinkUrl: ' ' })), - ).toBe(`ical:calendar-event-uid:${STARTS_AT}`); - }); - - it('falls back to the iCal uid when the link is not a string', () => { - expect(computeRealMeetingKey(buildInput({ conferenceLinkUrl: 42 }))).toBe( - `ical:calendar-event-uid:${STARTS_AT}`, - ); - }); - - it('falls back to the calendar event id when link and iCal uid are missing', () => { - expect( - computeRealMeetingKey( - buildInput({ conferenceLinkUrl: undefined, iCalUid: '' }), - ), - ).toBe('event:calendar-event-1'); - }); - - it('keeps link keys distinct across start times', () => { - expect(computeRealMeetingKey(buildInput({ startsAt: undefined }))).toBe( - 'link:meet.example.com/customer-sync:', - ); - }); -}); diff --git a/packages/twenty-apps/public/twenty-meeting-bot/src/logic-functions/domain/__tests__/is-call-recording-ingestion-complete.test.ts b/packages/twenty-apps/public/twenty-meeting-bot/src/logic-functions/domain/__tests__/is-call-recording-ingestion-complete.test.ts deleted file mode 100644 index a7052f6fa747c..0000000000000 --- a/packages/twenty-apps/public/twenty-meeting-bot/src/logic-functions/domain/__tests__/is-call-recording-ingestion-complete.test.ts +++ /dev/null @@ -1,59 +0,0 @@ -import { describe, expect, it } from 'vitest'; - -import { isCallRecordingIngestionComplete } from 'src/logic-functions/domain/is-call-recording-ingestion-complete.util'; - -const AUDIO_VALUE = [{ fileId: 'file-audio-1', label: 'audio.mp3' }]; -const VIDEO_VALUE = [{ fileId: 'file-video-1', label: 'video.mp4' }]; -const TRANSCRIPT_CONTENT = [{ participant: { id: 1 }, words: [] }]; - -describe('isCallRecordingIngestionComplete', () => { - it('is complete when transcript content and both media files are present', () => { - expect( - isCallRecordingIngestionComplete({ - transcript: TRANSCRIPT_CONTENT, - audio: AUDIO_VALUE, - video: VIDEO_VALUE, - }), - ).toBe(true); - }); - - it('is incomplete while the transcript holds a marker', () => { - expect( - isCallRecordingIngestionComplete({ - transcript: { - recallTranscriptId: 'recall-transcript-1', - status: 'PENDING', - }, - audio: AUDIO_VALUE, - video: VIDEO_VALUE, - }), - ).toBe(false); - }); - - it('is incomplete when the transcript is unset', () => { - expect( - isCallRecordingIngestionComplete({ - transcript: null, - audio: AUDIO_VALUE, - video: VIDEO_VALUE, - }), - ).toBe(false); - }); - - it('is incomplete while any media field is empty', () => { - expect( - isCallRecordingIngestionComplete({ - transcript: TRANSCRIPT_CONTENT, - audio: undefined, - video: VIDEO_VALUE, - }), - ).toBe(false); - expect( - isCallRecordingIngestionComplete({ - transcript: TRANSCRIPT_CONTENT, - audio: AUDIO_VALUE, - video: [], - }), - ).toBe(false); - }); -}); diff --git a/packages/twenty-apps/public/twenty-meeting-bot/src/logic-functions/domain/__tests__/is-call-recording-status-downgrade.test.ts b/packages/twenty-apps/public/twenty-meeting-bot/src/logic-functions/domain/__tests__/is-call-recording-status-downgrade.test.ts deleted file mode 100644 index 3e73245e19795..0000000000000 --- a/packages/twenty-apps/public/twenty-meeting-bot/src/logic-functions/domain/__tests__/is-call-recording-status-downgrade.test.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { describe, expect, it } from 'vitest'; - -import { isCallRecordingStatusDowngrade } from 'src/logic-functions/domain/is-call-recording-status-downgrade.util'; - -describe('isCallRecordingStatusDowngrade', () => { - it.each([ - ['SCHEDULED', 'JOINING', false], - ['JOINING', 'RECORDING', false], - ['RECORDING', 'PROCESSING', false], - ['PROCESSING', 'FAILED', false], - ['PROCESSING', 'COMPLETED', false], - ['RECORDING', 'RECORDING', false], - ['COMPLETED', 'RECORDING', true], - ['PROCESSING', 'JOINING', true], - ['FAILED', 'RECORDING', true], - ['JOINING', 'SCHEDULED', true], - ])('from %s to %s -> %s', (fromStatus, toStatus, expected) => { - expect(isCallRecordingStatusDowngrade({ fromStatus, toStatus })).toBe( - expected, - ); - }); - - it('never treats transitions from unknown statuses as downgrades', () => { - expect( - isCallRecordingStatusDowngrade({ - fromStatus: undefined, - toStatus: 'COMPLETED', - }), - ).toBe(false); - expect( - isCallRecordingStatusDowngrade({ - fromStatus: 'NOT_A_STATUS', - toStatus: 'SCHEDULED', - }), - ).toBe(false); - }); -}); diff --git a/packages/twenty-apps/public/twenty-meeting-bot/src/logic-functions/domain/__tests__/resolve-meeting-bot-policy-result.test.ts b/packages/twenty-apps/public/twenty-meeting-bot/src/logic-functions/domain/__tests__/resolve-meeting-bot-policy-result.test.ts deleted file mode 100644 index 4ed5739e37729..0000000000000 --- a/packages/twenty-apps/public/twenty-meeting-bot/src/logic-functions/domain/__tests__/resolve-meeting-bot-policy-result.test.ts +++ /dev/null @@ -1,120 +0,0 @@ -import { describe, expect, it } from 'vitest'; - -import { MeetingBotPreference } from 'src/constants/meeting-bot-preference'; -import { resolveMeetingBotPolicyResult } from 'src/logic-functions/domain/resolve-meeting-bot-policy-result.util'; - -const NOW = new Date('2026-01-01T12:00:00.000Z'); -const FUTURE_STARTS_AT = '2026-01-01T13:00:00.000Z'; -const FUTURE_ENDS_AT = '2026-01-01T14:00:00.000Z'; -const PAST_STARTS_AT = '2026-01-01T09:00:00.000Z'; -const PAST_ENDS_AT = '2026-01-01T10:00:00.000Z'; - -describe('resolveMeetingBotPolicyResult', () => { - it('requires a bot when preference is ON and the event is upcoming', () => { - expect( - resolveMeetingBotPolicyResult({ - input: { - meetingBotPreference: MeetingBotPreference.ON, - isCanceled: false, - startsAt: FUTURE_STARTS_AT, - endsAt: FUTURE_ENDS_AT, - conferenceLinkUrl: 'https://meet.example.com/team-sync', - }, - now: NOW, - }), - ).toEqual({ - shouldRequestBot: true, - reason: 'RECORDING_ENABLED', - }); - }); - - it('does not request a bot for ON when the meeting has no conference link', () => { - expect( - resolveMeetingBotPolicyResult({ - input: { - meetingBotPreference: MeetingBotPreference.ON, - isCanceled: false, - startsAt: FUTURE_STARTS_AT, - endsAt: FUTURE_ENDS_AT, - conferenceLinkUrl: undefined, - }, - now: NOW, - }), - ).toEqual({ - shouldRequestBot: false, - reason: 'MISSING_CONFERENCE_LINK', - }); - }); - - it('requires a bot without an event preference override', () => { - expect( - resolveMeetingBotPolicyResult({ - input: { - meetingBotPreference: undefined, - isCanceled: false, - startsAt: FUTURE_STARTS_AT, - endsAt: FUTURE_ENDS_AT, - conferenceLinkUrl: 'https://meet.example.com/team-sync', - }, - now: NOW, - }), - ).toEqual({ - shouldRequestBot: true, - reason: 'RECORDING_ENABLED', - }); - }); - - it('lets an OFF event preference opt out of workspace auto-recording', () => { - expect( - resolveMeetingBotPolicyResult({ - input: { - meetingBotPreference: MeetingBotPreference.OFF, - isCanceled: false, - startsAt: FUTURE_STARTS_AT, - endsAt: FUTURE_ENDS_AT, - conferenceLinkUrl: 'https://meet.example.com/team-sync', - }, - now: NOW, - }), - ).toEqual({ - shouldRequestBot: false, - reason: 'PREFERENCE_OFF', - }); - }); - - it('does not request a bot for a meeting that already ended', () => { - expect( - resolveMeetingBotPolicyResult({ - input: { - meetingBotPreference: undefined, - isCanceled: false, - startsAt: PAST_STARTS_AT, - endsAt: PAST_ENDS_AT, - conferenceLinkUrl: 'https://meet.example.com/team-sync', - }, - now: NOW, - }), - ).toEqual({ - shouldRequestBot: false, - reason: 'EVENT_NOT_UPCOMING', - }); - }); - - it('does not request a bot for a canceled meeting', () => { - expect( - resolveMeetingBotPolicyResult({ - input: { - meetingBotPreference: undefined, - isCanceled: true, - startsAt: FUTURE_STARTS_AT, - endsAt: FUTURE_ENDS_AT, - conferenceLinkUrl: 'https://meet.example.com/team-sync', - }, - now: NOW, - }), - ).toEqual({ - shouldRequestBot: false, - reason: 'EVENT_CANCELED', - }); - }); -}); diff --git a/packages/twenty-apps/public/twenty-meeting-bot/src/logic-functions/domain/__tests__/should-complete-call-recording-ingestion.test.ts b/packages/twenty-apps/public/twenty-meeting-bot/src/logic-functions/domain/__tests__/should-complete-call-recording-ingestion.test.ts deleted file mode 100644 index 87c95e643a145..0000000000000 --- a/packages/twenty-apps/public/twenty-meeting-bot/src/logic-functions/domain/__tests__/should-complete-call-recording-ingestion.test.ts +++ /dev/null @@ -1,102 +0,0 @@ -import { describe, expect, it } from 'vitest'; - -import { CallRecordingStatus } from 'src/logic-functions/constants/call-recording-status'; -import { shouldCompleteCallRecordingIngestion } from 'src/logic-functions/domain/should-complete-call-recording-ingestion.util'; - -const filledTranscript = [{ participant: { id: 1 }, words: [] }]; -const filledAudio = [{ fileId: 'file-audio-1', label: 'audio.mp3' }]; -const filledVideo = [{ fileId: 'file-video-1', label: 'video.mp4' }]; - -describe('shouldCompleteCallRecordingIngestion', () => { - it('requires complete artifacts and billable timestamps before completion', () => { - expect( - shouldCompleteCallRecordingIngestion({ - current: { - status: CallRecordingStatus.PROCESSING, - startedAt: '2026-06-10T09:00:00.000Z', - endedAt: '2026-06-10T10:00:00.000Z', - transcript: filledTranscript, - audio: filledAudio, - video: filledVideo, - }, - updateData: {}, - }), - ).toBe(true); - - expect( - shouldCompleteCallRecordingIngestion({ - current: { - status: CallRecordingStatus.PROCESSING, - endedAt: '2026-06-10T10:00:00.000Z', - transcript: filledTranscript, - audio: filledAudio, - video: filledVideo, - }, - updateData: {}, - }), - ).toBe(false); - - expect( - shouldCompleteCallRecordingIngestion({ - current: { - status: CallRecordingStatus.PROCESSING, - transcript: filledTranscript, - audio: filledAudio, - video: filledVideo, - }, - updateData: { - startedAt: '2026-06-10T09:00:00.000Z', - endedAt: '2026-06-10T10:00:00.000Z', - }, - }), - ).toBe(true); - - expect( - shouldCompleteCallRecordingIngestion({ - current: { - status: CallRecordingStatus.PROCESSING, - startedAt: '2026-06-10T10:00:00.000Z', - endedAt: '2026-06-10T09:00:00.000Z', - transcript: filledTranscript, - audio: filledAudio, - video: filledVideo, - }, - updateData: {}, - }), - ).toBe(false); - }); - - it('does not complete a persisted failed recording', () => { - expect( - shouldCompleteCallRecordingIngestion({ - current: { - status: CallRecordingStatus.FAILED, - startedAt: '2026-06-10T09:00:00.000Z', - endedAt: '2026-06-10T10:00:00.000Z', - transcript: filledTranscript, - audio: filledAudio, - video: filledVideo, - }, - updateData: {}, - }), - ).toBe(false); - }); - - it('does not complete when the incoming update marks the recording as failed', () => { - expect( - shouldCompleteCallRecordingIngestion({ - current: { - status: CallRecordingStatus.PROCESSING, - startedAt: '2026-06-10T09:00:00.000Z', - endedAt: '2026-06-10T10:00:00.000Z', - transcript: filledTranscript, - audio: filledAudio, - video: filledVideo, - }, - updateData: { - status: CallRecordingStatus.FAILED, - }, - }), - ).toBe(false); - }); -}); diff --git a/packages/twenty-apps/public/twenty-meeting-bot/src/logic-functions/domain/aggregate-meeting-bot-policy-results-by-meeting.util.ts b/packages/twenty-apps/public/twenty-meeting-bot/src/logic-functions/domain/aggregate-meeting-bot-policy-results-by-meeting.util.ts deleted file mode 100644 index 07c7975b8c14d..0000000000000 --- a/packages/twenty-apps/public/twenty-meeting-bot/src/logic-functions/domain/aggregate-meeting-bot-policy-results-by-meeting.util.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { type MeetingBotPolicyResultForCalendarEvent } from 'src/logic-functions/types/meeting-bot-policy-result-for-calendar-event.type'; -import { type MeetingBotPolicyResultForMeeting } from 'src/logic-functions/types/meeting-bot-policy-result-for-meeting.type'; - -type MeetingBotPolicyResultForMeetingInput = Pick< - MeetingBotPolicyResultForCalendarEvent, - 'calendarEventId' | 'realMeetingKey' | 'shouldRequestBot' ->; - -export const aggregateMeetingBotPolicyResultsByMeeting = ( - perCalendarEventPolicyResults: MeetingBotPolicyResultForMeetingInput[], -): MeetingBotPolicyResultForMeeting[] => { - const meetingPolicyResultsByMeetingKey = new Map< - string, - MeetingBotPolicyResultForMeeting - >(); - - for (const { - calendarEventId, - realMeetingKey, - shouldRequestBot, - } of perCalendarEventPolicyResults) { - const meetingPolicyResult = meetingPolicyResultsByMeetingKey.get( - realMeetingKey, - ) ?? { - realMeetingKey, - shouldRequestBot: false, - calendarEventIds: [], - requestingCalendarEventIds: [], - }; - - meetingPolicyResult.calendarEventIds.push(calendarEventId); - - if (shouldRequestBot) { - meetingPolicyResult.shouldRequestBot = true; - meetingPolicyResult.requestingCalendarEventIds.push(calendarEventId); - } - - meetingPolicyResultsByMeetingKey.set(realMeetingKey, meetingPolicyResult); - } - - return [...meetingPolicyResultsByMeetingKey.values()]; -}; diff --git a/packages/twenty-apps/public/twenty-meeting-bot/src/logic-functions/domain/build-failed-transcript-marker.util.ts b/packages/twenty-apps/public/twenty-meeting-bot/src/logic-functions/domain/build-failed-transcript-marker.util.ts deleted file mode 100644 index c7719537f67df..0000000000000 --- a/packages/twenty-apps/public/twenty-meeting-bot/src/logic-functions/domain/build-failed-transcript-marker.util.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { type TranscriptMarker } from 'src/logic-functions/types/transcript-marker.type'; - -export const buildFailedTranscriptMarker = ({ - recallTranscriptId, - subCode, -}: { - recallTranscriptId: string | null; - subCode: string | null; -}): TranscriptMarker => ({ - recallTranscriptId, - status: 'FAILED', - subCode, -}); diff --git a/packages/twenty-apps/public/twenty-meeting-bot/src/logic-functions/domain/build-meeting-bot-policy-result.util.ts b/packages/twenty-apps/public/twenty-meeting-bot/src/logic-functions/domain/build-meeting-bot-policy-result.util.ts deleted file mode 100644 index fc181bcd06277..0000000000000 --- a/packages/twenty-apps/public/twenty-meeting-bot/src/logic-functions/domain/build-meeting-bot-policy-result.util.ts +++ /dev/null @@ -1,53 +0,0 @@ -import { MeetingBotPreference } from 'src/constants/meeting-bot-preference'; -import { type MeetingBotPolicyCalendarEventInput } from 'src/logic-functions/types/meeting-bot-policy-calendar-event-input.type'; -import { type MeetingBotPolicyResultForCalendarEvent } from 'src/logic-functions/types/meeting-bot-policy-result-for-calendar-event.type'; -import { computeRealMeetingKey } from 'src/logic-functions/domain/compute-real-meeting-key.util'; -import { resolveMeetingBotPolicyResult } from 'src/logic-functions/domain/resolve-meeting-bot-policy-result.util'; - -export const buildMeetingBotPolicyResult = ( - calendarEvent: MeetingBotPolicyCalendarEventInput, - now: Date, -): MeetingBotPolicyResultForCalendarEvent => { - const realMeetingKey = computeRealMeetingKey({ - calendarEventId: calendarEvent.id, - conferenceLinkUrl: calendarEvent.conferenceLinkUrl, - iCalUid: calendarEvent.iCalUid, - startsAt: calendarEvent.startsAt, - }); - - const meetingBotPreference = normalizeMeetingBotPreference( - calendarEvent.meetingBotPreference, - ); - - const policyResult = resolveMeetingBotPolicyResult({ - input: { - meetingBotPreference, - isCanceled: calendarEvent.isCanceled, - startsAt: calendarEvent.startsAt, - endsAt: calendarEvent.endsAt, - conferenceLinkUrl: calendarEvent.conferenceLinkUrl, - }, - now, - }); - - return { - calendarEventId: calendarEvent.id, - meetingBotPreference, - realMeetingKey, - ...policyResult, - }; -}; - -const normalizeMeetingBotPreference = ( - meetingBotPreference: string | undefined, -): MeetingBotPreference | undefined => - isMeetingBotPreference(meetingBotPreference) - ? meetingBotPreference - : undefined; - -const isMeetingBotPreference = ( - meetingBotPreference: string | undefined, -): meetingBotPreference is MeetingBotPreference => - Object.values(MeetingBotPreference).some( - (preference) => preference === meetingBotPreference, - ); diff --git a/packages/twenty-apps/public/twenty-meeting-bot/src/logic-functions/domain/build-pending-transcript-marker.util.ts b/packages/twenty-apps/public/twenty-meeting-bot/src/logic-functions/domain/build-pending-transcript-marker.util.ts deleted file mode 100644 index ae65c4b55608d..0000000000000 --- a/packages/twenty-apps/public/twenty-meeting-bot/src/logic-functions/domain/build-pending-transcript-marker.util.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { type TranscriptMarker } from 'src/logic-functions/types/transcript-marker.type'; - -export const buildPendingTranscriptMarker = ({ - recallTranscriptId, - requestedAt, -}: { - recallTranscriptId: string; - requestedAt: string; -}): TranscriptMarker => ({ - recallTranscriptId, - status: 'PENDING', - requestedAt, -}); diff --git a/packages/twenty-apps/public/twenty-meeting-bot/src/logic-functions/domain/build-recall-bot-metadata.util.ts b/packages/twenty-apps/public/twenty-meeting-bot/src/logic-functions/domain/build-recall-bot-metadata.util.ts deleted file mode 100644 index ce8f6b240a74e..0000000000000 --- a/packages/twenty-apps/public/twenty-meeting-bot/src/logic-functions/domain/build-recall-bot-metadata.util.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { type MeetingRecording } from 'src/logic-functions/types/meeting-recording.type'; -import { type RecallBotMetadata } from 'src/logic-functions/types/recall-bot-metadata.type'; -import { computeRealMeetingKey } from 'src/logic-functions/domain/compute-real-meeting-key.util'; - -export const buildRecallBotMetadata = ({ - callRecording, - calendarEvent, - workspaceId, -}: MeetingRecording & { workspaceId: string }): RecallBotMetadata => { - return { - twentyWorkspaceId: workspaceId, - twentyCallRecordingId: callRecording.id, - twentyCalendarEventId: calendarEvent.id, - twentyRealMeetingKey: computeRealMeetingKey({ - calendarEventId: calendarEvent.id, - conferenceLinkUrl: calendarEvent.conferenceLinkUrl, - iCalUid: calendarEvent.iCalUid, - startsAt: calendarEvent.startsAt, - }), - }; -}; diff --git a/packages/twenty-apps/public/twenty-meeting-bot/src/logic-functions/domain/build-transcript-failure-reason.util.ts b/packages/twenty-apps/public/twenty-meeting-bot/src/logic-functions/domain/build-transcript-failure-reason.util.ts deleted file mode 100644 index aab66570b5999..0000000000000 --- a/packages/twenty-apps/public/twenty-meeting-bot/src/logic-functions/domain/build-transcript-failure-reason.util.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { isNull } from '@sniptt/guards'; - -export const buildTranscriptFailureReason = (subCode: string | null): string => { - return isNull(subCode) - ? 'transcript_failed' - : `transcript_failed:${subCode}`; -}; diff --git a/packages/twenty-apps/public/twenty-meeting-bot/src/logic-functions/domain/compute-call-recording-charge.util.ts b/packages/twenty-apps/public/twenty-meeting-bot/src/logic-functions/domain/compute-call-recording-charge.util.ts deleted file mode 100644 index b7d58291bb521..0000000000000 --- a/packages/twenty-apps/public/twenty-meeting-bot/src/logic-functions/domain/compute-call-recording-charge.util.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { isUndefined } from '@sniptt/guards'; - -import { CALL_RECORDING_MICRO_CREDITS_PER_HOUR } from 'src/logic-functions/constants/call-recording-micro-credits-per-hour'; -import { MILLISECONDS_PER_MINUTE } from 'src/logic-functions/constants/milliseconds-per-minute'; - -const MILLISECONDS_PER_HOUR = 3_600_000; - -export type CallRecordingCharge = { - creditsUsedMicro: number; - quantityMinutes: number; -}; - -export const computeCallRecordingCharge = ({ - startedAt, - endedAt, -}: { - startedAt: string | undefined; - endedAt: string | undefined; -}): CallRecordingCharge | undefined => { - if (isUndefined(startedAt) || isUndefined(endedAt)) { - return undefined; - } - - const durationMilliseconds = - new Date(endedAt).getTime() - new Date(startedAt).getTime(); - - if (!Number.isFinite(durationMilliseconds) || durationMilliseconds <= 0) { - return undefined; - } - - return { - creditsUsedMicro: Math.round( - (durationMilliseconds / MILLISECONDS_PER_HOUR) * - CALL_RECORDING_MICRO_CREDITS_PER_HOUR, - ), - quantityMinutes: Math.max( - 1, - Math.round(durationMilliseconds / MILLISECONDS_PER_MINUTE), - ), - }; -}; diff --git a/packages/twenty-apps/public/twenty-meeting-bot/src/logic-functions/domain/compute-call-recording-id-for-meeting.util.ts b/packages/twenty-apps/public/twenty-meeting-bot/src/logic-functions/domain/compute-call-recording-id-for-meeting.util.ts deleted file mode 100644 index 5df420dd35257..0000000000000 --- a/packages/twenty-apps/public/twenty-meeting-bot/src/logic-functions/domain/compute-call-recording-id-for-meeting.util.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { createHash } from 'crypto'; - -// Same meeting key → same id: the primary key serializes concurrent creates. -export const computeCallRecordingIdForMeeting = ( - realMeetingKey: string, -): string => { - const bytes = createHash('sha256').update(realMeetingKey).digest(); - - // v4 version/variant bits so server-side UUID validation accepts the hash. - bytes[6] = (bytes[6] & 0x0f) | 0x40; - bytes[8] = (bytes[8] & 0x3f) | 0x80; - - const hex = bytes.subarray(0, 16).toString('hex'); - - return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`; -}; diff --git a/packages/twenty-apps/public/twenty-meeting-bot/src/logic-functions/domain/compute-real-meeting-key.util.ts b/packages/twenty-apps/public/twenty-meeting-bot/src/logic-functions/domain/compute-real-meeting-key.util.ts deleted file mode 100644 index 56f59cf7f3a2a..0000000000000 --- a/packages/twenty-apps/public/twenty-meeting-bot/src/logic-functions/domain/compute-real-meeting-key.util.ts +++ /dev/null @@ -1,48 +0,0 @@ -import { isUndefined } from '@sniptt/guards'; - -import { isNonEmptyString } from 'src/logic-functions/utils/is-non-empty-string.util'; - -type ComputeRealMeetingKeyInput = { - calendarEventId: string; - conferenceLinkUrl: unknown; - iCalUid: string | undefined; - startsAt: string | undefined; -}; - -export const computeRealMeetingKey = ({ - calendarEventId, - conferenceLinkUrl, - iCalUid, - startsAt, -}: ComputeRealMeetingKeyInput): string => { - const normalizedConferenceLink = normalizeConferenceLink(conferenceLinkUrl); - - if (!isUndefined(normalizedConferenceLink)) { - return `link:${normalizedConferenceLink}:${startsAt ?? ''}`; - } - - if (isNonEmptyString(iCalUid)) { - return `ical:${iCalUid}:${startsAt ?? ''}`; - } - - return `event:${calendarEventId}`; -}; - -const normalizeConferenceLink = ( - conferenceLinkUrl: unknown, -): string | undefined => { - if (!isNonEmptyString(conferenceLinkUrl)) { - return undefined; - } - - const withoutProtocol = conferenceLinkUrl - .trim() - .toLowerCase() - .replace(/^https?:\/\//, '') - .replace(/^www\./, ''); - - const withoutQueryAndFragment = withoutProtocol.split(/[?#]/)[0]; - const withoutTrailingSlash = withoutQueryAndFragment.replace(/\/+$/, ''); - - return withoutTrailingSlash === '' ? undefined : withoutTrailingSlash; -}; diff --git a/packages/twenty-apps/public/twenty-meeting-bot/src/logic-functions/domain/compute-recall-bot-join-at.util.ts b/packages/twenty-apps/public/twenty-meeting-bot/src/logic-functions/domain/compute-recall-bot-join-at.util.ts deleted file mode 100644 index cfef069543a73..0000000000000 --- a/packages/twenty-apps/public/twenty-meeting-bot/src/logic-functions/domain/compute-recall-bot-join-at.util.ts +++ /dev/null @@ -1,34 +0,0 @@ -import { DEFAULT_MEETING_BOT_JOIN_EARLY_MINUTES } from 'src/logic-functions/constants/default-meeting-bot-join-early-minutes'; -import { MILLISECONDS_PER_MINUTE } from 'src/logic-functions/constants/milliseconds-per-minute'; -import { MEETING_BOT_JOIN_EARLY_MINUTES_ENV_VAR_NAME } from 'src/logic-functions/constants/meeting-bot-join-early-minutes-env-var-name'; -import { getApplicationVariableValue } from 'src/logic-functions/utils/get-application-variable-value.util'; -import { isNonEmptyString } from 'src/logic-functions/utils/is-non-empty-string.util'; - -export const computeRecallBotJoinAt = (meetingStartsAt: string): string => { - const meetingStartTimeInMilliseconds = new Date(meetingStartsAt).getTime(); - - if (Number.isNaN(meetingStartTimeInMilliseconds)) { - return meetingStartsAt; - } - - return new Date( - meetingStartTimeInMilliseconds - - getRecallBotJoinEarlyMinutes() * MILLISECONDS_PER_MINUTE, - ).toISOString(); -}; - -const getRecallBotJoinEarlyMinutes = (): number => { - const rawValue = getApplicationVariableValue( - MEETING_BOT_JOIN_EARLY_MINUTES_ENV_VAR_NAME, - ); - - if (!isNonEmptyString(rawValue)) { - return DEFAULT_MEETING_BOT_JOIN_EARLY_MINUTES; - } - - const joinEarlyMinutes = Number(rawValue.trim()); - - return Number.isInteger(joinEarlyMinutes) && joinEarlyMinutes >= 0 - ? joinEarlyMinutes - : DEFAULT_MEETING_BOT_JOIN_EARLY_MINUTES; -}; diff --git a/packages/twenty-apps/public/twenty-meeting-bot/src/logic-functions/domain/is-call-recording-ingestion-complete.util.ts b/packages/twenty-apps/public/twenty-meeting-bot/src/logic-functions/domain/is-call-recording-ingestion-complete.util.ts deleted file mode 100644 index ec32b40758dff..0000000000000 --- a/packages/twenty-apps/public/twenty-meeting-bot/src/logic-functions/domain/is-call-recording-ingestion-complete.util.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { isNonEmptyArray, isNull, isUndefined } from '@sniptt/guards'; - -import { type FilesFieldValue } from 'src/logic-functions/types/files-field-value.type'; -import { parseTranscriptMarker } from 'src/logic-functions/domain/parse-transcript-marker.util'; - -export const isCallRecordingIngestionComplete = ({ - transcript, - audio, - video, -}: { - transcript: unknown; - audio: FilesFieldValue | undefined; - video: FilesFieldValue | undefined; -}): boolean => - !isNull(transcript) && - !isUndefined(transcript) && - isUndefined(parseTranscriptMarker(transcript)) && - isNonEmptyArray(audio) && - isNonEmptyArray(video); diff --git a/packages/twenty-apps/public/twenty-meeting-bot/src/logic-functions/domain/is-call-recording-status-downgrade.util.ts b/packages/twenty-apps/public/twenty-meeting-bot/src/logic-functions/domain/is-call-recording-status-downgrade.util.ts deleted file mode 100644 index 45843102bb083..0000000000000 --- a/packages/twenty-apps/public/twenty-meeting-bot/src/logic-functions/domain/is-call-recording-status-downgrade.util.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { isUndefined } from '@sniptt/guards'; - -import { CallRecordingStatus } from 'src/logic-functions/constants/call-recording-status'; - -// Deliveries are unordered; a late event must never move status backwards. -const CALL_RECORDING_STATUS_PROGRESSION: Record = { - [CallRecordingStatus.SCHEDULED]: 0, - [CallRecordingStatus.JOINING]: 1, - [CallRecordingStatus.RECORDING]: 2, - [CallRecordingStatus.PROCESSING]: 3, - [CallRecordingStatus.FAILED]: 4, - [CallRecordingStatus.COMPLETED]: 5, -}; - -const getCallRecordingStatusRank = (status: string): number | undefined => - status in CALL_RECORDING_STATUS_PROGRESSION - ? CALL_RECORDING_STATUS_PROGRESSION[status as CallRecordingStatus] - : undefined; - -export const isCallRecordingStatusDowngrade = ({ - fromStatus, - toStatus, -}: { - fromStatus: string | undefined; - toStatus: string; -}): boolean => { - const fromRank = isUndefined(fromStatus) - ? undefined - : getCallRecordingStatusRank(fromStatus); - const toRank = getCallRecordingStatusRank(toStatus); - - if (isUndefined(fromRank) || isUndefined(toRank)) { - return false; - } - - return toRank < fromRank; -}; diff --git a/packages/twenty-apps/public/twenty-meeting-bot/src/logic-functions/domain/is-recall-recording-done-signal.util.ts b/packages/twenty-apps/public/twenty-meeting-bot/src/logic-functions/domain/is-recall-recording-done-signal.util.ts deleted file mode 100644 index 50b2efef7c7cb..0000000000000 --- a/packages/twenty-apps/public/twenty-meeting-bot/src/logic-functions/domain/is-recall-recording-done-signal.util.ts +++ /dev/null @@ -1,13 +0,0 @@ -export const isRecallRecordingDoneSignal = ({ - event, - statusCode, -}: { - event: string; - statusCode: string | undefined; -}): boolean => { - return ( - event === 'recording.done' || - event === 'recording.failed' || - statusCode === 'done' - ); -}; diff --git a/packages/twenty-apps/public/twenty-meeting-bot/src/logic-functions/domain/map-recall-status-code-to-call-recording-status.util.ts b/packages/twenty-apps/public/twenty-meeting-bot/src/logic-functions/domain/map-recall-status-code-to-call-recording-status.util.ts deleted file mode 100644 index 9af31afc32b69..0000000000000 --- a/packages/twenty-apps/public/twenty-meeting-bot/src/logic-functions/domain/map-recall-status-code-to-call-recording-status.util.ts +++ /dev/null @@ -1,26 +0,0 @@ -import { CallRecordingStatus } from 'src/logic-functions/constants/call-recording-status'; - -export const mapRecallStatusCodeToCallRecordingStatus = ( - statusCode: string | undefined, -): CallRecordingStatus | undefined => { - switch (statusCode) { - case 'joining_call': - case 'in_waiting_room': - return CallRecordingStatus.JOINING; - case 'in_call_not_recording': - case 'recording_permission_allowed': - case 'in_call_recording': - return CallRecordingStatus.RECORDING; - // 'done' stays PROCESSING: COMPLETED is set only after all artifacts are ingested. - case 'call_ended': - case 'analysis_done': - case 'done': - return CallRecordingStatus.PROCESSING; - case 'fatal': - case 'analysis_failed': - case 'recording_permission_denied': - return CallRecordingStatus.FAILED; - default: - return undefined; - } -}; diff --git a/packages/twenty-apps/public/twenty-meeting-bot/src/logic-functions/domain/parse-transcript-marker.util.ts b/packages/twenty-apps/public/twenty-meeting-bot/src/logic-functions/domain/parse-transcript-marker.util.ts deleted file mode 100644 index b2806f2a7b80c..0000000000000 --- a/packages/twenty-apps/public/twenty-meeting-bot/src/logic-functions/domain/parse-transcript-marker.util.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { isString, isUndefined } from '@sniptt/guards'; - -import { type TranscriptMarker } from 'src/logic-functions/types/transcript-marker.type'; -import { asRecord } from 'src/logic-functions/utils/as-record.util'; - -export const parseTranscriptMarker = ( - transcript: unknown, -): TranscriptMarker | undefined => { - const candidate = asRecord(transcript); - - if (isUndefined(candidate)) { - return undefined; - } - - if (candidate.status !== 'PENDING' && candidate.status !== 'FAILED') { - return undefined; - } - - return { - recallTranscriptId: isString(candidate.recallTranscriptId) - ? candidate.recallTranscriptId - : null, - status: candidate.status, - ...(isString(candidate.requestedAt) - ? { requestedAt: candidate.requestedAt } - : {}), - ...(isString(candidate.subCode) ? { subCode: candidate.subCode } : {}), - }; -}; diff --git a/packages/twenty-apps/public/twenty-meeting-bot/src/logic-functions/domain/resolve-meeting-bot-policy-result.util.ts b/packages/twenty-apps/public/twenty-meeting-bot/src/logic-functions/domain/resolve-meeting-bot-policy-result.util.ts deleted file mode 100644 index d90cc1906fd68..0000000000000 --- a/packages/twenty-apps/public/twenty-meeting-bot/src/logic-functions/domain/resolve-meeting-bot-policy-result.util.ts +++ /dev/null @@ -1,72 +0,0 @@ -import { MeetingBotPreference } from 'src/constants/meeting-bot-preference'; -import { type MeetingBotPolicyInput } from 'src/logic-functions/types/meeting-bot-policy-input.type'; -import { isNonEmptyString } from 'src/logic-functions/utils/is-non-empty-string.util'; -import { type MeetingBotPolicyNotRequiredReason } from 'src/logic-functions/types/meeting-bot-policy-not-required-reason.type'; -import { type MeetingBotPolicyRequiredReason } from 'src/logic-functions/types/meeting-bot-policy-required-reason.type'; -import { type MeetingBotPolicyResult } from 'src/logic-functions/types/meeting-bot-policy-result.type'; - -type ResolveMeetingBotPolicyResultArgs = { - input: MeetingBotPolicyInput; - now: Date; -}; - -export const resolveMeetingBotPolicyResult = ({ - input, - now, -}: ResolveMeetingBotPolicyResultArgs): MeetingBotPolicyResult => { - if (input.isCanceled) { - return botNotRequired('EVENT_CANCELED'); - } - - if (input.meetingBotPreference === MeetingBotPreference.OFF) { - return botNotRequired('PREFERENCE_OFF'); - } - - if (!isNonEmptyString(input.conferenceLinkUrl)) { - return botNotRequired('MISSING_CONFERENCE_LINK'); - } - - if ( - !isCalendarEventInFuture({ - startsAt: input.startsAt, - endsAt: input.endsAt, - now, - }) - ) { - return botNotRequired('EVENT_NOT_UPCOMING'); - } - - return botRequired('RECORDING_ENABLED'); -}; - -const isCalendarEventInFuture = ({ - startsAt, - endsAt, - now, -}: { - startsAt: string | undefined; - endsAt: string | undefined; - now: Date; -}): boolean => { - const reference = endsAt ?? startsAt; - - if (!isNonEmptyString(reference)) { - return false; - } - - const referenceTime = new Date(reference).getTime(); - - if (Number.isNaN(referenceTime)) { - return false; - } - - return referenceTime > now.getTime(); -}; - -const botRequired = ( - reason: MeetingBotPolicyRequiredReason, -): MeetingBotPolicyResult => ({ shouldRequestBot: true, reason }); - -const botNotRequired = ( - reason: MeetingBotPolicyNotRequiredReason, -): MeetingBotPolicyResult => ({ shouldRequestBot: false, reason }); diff --git a/packages/twenty-apps/public/twenty-meeting-bot/src/logic-functions/domain/should-complete-call-recording-ingestion.util.ts b/packages/twenty-apps/public/twenty-meeting-bot/src/logic-functions/domain/should-complete-call-recording-ingestion.util.ts deleted file mode 100644 index 5e2f827e45891..0000000000000 --- a/packages/twenty-apps/public/twenty-meeting-bot/src/logic-functions/domain/should-complete-call-recording-ingestion.util.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { CallRecordingStatus } from 'src/logic-functions/constants/call-recording-status'; -import { type FilesFieldValue } from 'src/logic-functions/types/files-field-value.type'; -import { computeCallRecordingCharge } from 'src/logic-functions/domain/compute-call-recording-charge.util'; -import { isCallRecordingIngestionComplete } from 'src/logic-functions/domain/is-call-recording-ingestion-complete.util'; -import { type CallRecordingUpdateFields } from 'src/logic-functions/data/update-call-recording.util'; - -export const shouldCompleteCallRecordingIngestion = ({ - current, - updateData, -}: { - current: { - status?: string; - startedAt?: string; - endedAt?: string; - transcript?: unknown; - audio?: FilesFieldValue; - video?: FilesFieldValue; - }; - updateData: CallRecordingUpdateFields; -}): boolean => - current.status !== CallRecordingStatus.COMPLETED && - current.status !== CallRecordingStatus.FAILED && - updateData.status !== CallRecordingStatus.FAILED && - computeCallRecordingCharge({ - startedAt: updateData.startedAt ?? current.startedAt, - endedAt: updateData.endedAt ?? current.endedAt, - }) !== undefined && - isCallRecordingIngestionComplete({ - transcript: updateData.transcript ?? current.transcript, - audio: updateData.audio ?? current.audio, - video: updateData.video ?? current.video, - }); diff --git a/packages/twenty-apps/public/twenty-meeting-bot/src/logic-functions/flows/__tests__/charge-completed-call-recording.test.ts b/packages/twenty-apps/public/twenty-meeting-bot/src/logic-functions/flows/__tests__/charge-completed-call-recording.test.ts deleted file mode 100644 index f1e9077ed873c..0000000000000 --- a/packages/twenty-apps/public/twenty-meeting-bot/src/logic-functions/flows/__tests__/charge-completed-call-recording.test.ts +++ /dev/null @@ -1,45 +0,0 @@ -import { beforeEach, describe, expect, it, vi } from 'vitest'; - -import { chargeCompletedCallRecording } from 'src/logic-functions/flows/charge-completed-call-recording.util'; - -const chargeCreditsMock = vi.hoisted(() => vi.fn()); - -vi.mock('twenty-sdk/billing', () => ({ - chargeCredits: chargeCreditsMock, -})); - -describe('chargeCompletedCallRecording', () => { - beforeEach(() => { - vi.spyOn(console, 'warn').mockImplementation(() => {}); - chargeCreditsMock.mockReset(); - chargeCreditsMock.mockResolvedValue(undefined); - }); - - it('charges prorated micro-credits with the recording duration in minutes', async () => { - await chargeCompletedCallRecording({ - callRecordingId: 'call-recording-1', - startedAt: '2026-06-10T09:00:00.000Z', - endedAt: '2026-06-10T09:30:00.000Z', - }); - - expect(chargeCreditsMock).toHaveBeenCalledWith({ - creditsUsedMicro: 500_000, - quantity: 30, - operationType: 'CALL_RECORDING', - resourceContext: 'recall', - }); - }); - - it('skips and warns loudly when timestamps are unusable', async () => { - await chargeCompletedCallRecording({ - callRecordingId: 'call-recording-1', - startedAt: undefined, - endedAt: '2026-06-10T09:30:00.000Z', - }); - - expect(chargeCreditsMock).not.toHaveBeenCalled(); - expect(console.warn).toHaveBeenCalledWith( - expect.stringContaining('will not be billed'), - ); - }); -}); diff --git a/packages/twenty-apps/public/twenty-meeting-bot/src/logic-functions/flows/__tests__/complete-and-charge-call-recording.test.ts b/packages/twenty-apps/public/twenty-meeting-bot/src/logic-functions/flows/__tests__/complete-and-charge-call-recording.test.ts deleted file mode 100644 index 42c48e63cf6a7..0000000000000 --- a/packages/twenty-apps/public/twenty-meeting-bot/src/logic-functions/flows/__tests__/complete-and-charge-call-recording.test.ts +++ /dev/null @@ -1,61 +0,0 @@ -import { beforeEach, describe, expect, it, vi } from 'vitest'; - -const completeCallRecordingIngestionMock = vi.hoisted(() => vi.fn()); -const chargeCompletedCallRecordingMock = vi.hoisted(() => vi.fn()); - -vi.mock( - 'src/logic-functions/data/complete-call-recording-ingestion.util', - () => ({ - completeCallRecordingIngestion: completeCallRecordingIngestionMock, - }), -); - -vi.mock( - 'src/logic-functions/flows/charge-completed-call-recording.util', - () => ({ - chargeCompletedCallRecording: chargeCompletedCallRecordingMock, - }), -); - -import { completeAndChargeCallRecording } from 'src/logic-functions/flows/complete-and-charge-call-recording.util'; - -describe('completeAndChargeCallRecording', () => { - beforeEach(() => { - vi.clearAllMocks(); - }); - - it('charges exactly once when this path wins the completion claim', async () => { - completeCallRecordingIngestionMock.mockResolvedValue(true); - - const claimed = await completeAndChargeCallRecording({} as never, { - id: 'call-recording-1', - startedAt: '2026-06-10T12:00:00.000Z', - endedAt: '2026-06-10T13:00:00.000Z', - }); - - expect(claimed).toBe(true); - expect(completeCallRecordingIngestionMock).toHaveBeenCalledWith( - {}, - { id: 'call-recording-1' }, - ); - expect(chargeCompletedCallRecordingMock).toHaveBeenCalledTimes(1); - expect(chargeCompletedCallRecordingMock).toHaveBeenCalledWith({ - callRecordingId: 'call-recording-1', - startedAt: '2026-06-10T12:00:00.000Z', - endedAt: '2026-06-10T13:00:00.000Z', - }); - }); - - it('does not charge when another path already completed the recording', async () => { - completeCallRecordingIngestionMock.mockResolvedValue(false); - - const claimed = await completeAndChargeCallRecording({} as never, { - id: 'call-recording-1', - startedAt: '2026-06-10T12:00:00.000Z', - endedAt: '2026-06-10T13:00:00.000Z', - }); - - expect(claimed).toBe(false); - expect(chargeCompletedCallRecordingMock).not.toHaveBeenCalled(); - }); -}); diff --git a/packages/twenty-apps/public/twenty-meeting-bot/src/logic-functions/flows/__tests__/converge-diverged-call-recordings.test.ts b/packages/twenty-apps/public/twenty-meeting-bot/src/logic-functions/flows/__tests__/converge-diverged-call-recordings.test.ts deleted file mode 100644 index 7981d02a5b736..0000000000000 --- a/packages/twenty-apps/public/twenty-meeting-bot/src/logic-functions/flows/__tests__/converge-diverged-call-recordings.test.ts +++ /dev/null @@ -1,729 +0,0 @@ -import { type CoreApiClient } from 'twenty-client-sdk/core'; -import { beforeEach, describe, expect, it, vi } from 'vitest'; - -import { convergeDivergedCallRecordings } from 'src/logic-functions/flows/converge-diverged-call-recordings.util'; - -const getRecallBotMock = vi.hoisted(() => vi.fn()); -const listRecallTranscriptsMock = vi.hoisted(() => vi.fn()); -const createAsyncRecallTranscriptMock = vi.hoisted(() => vi.fn()); -const downloadTranscriptMock = vi.hoisted(() => vi.fn()); -const ingestCallRecordingMediaMock = vi.hoisted(() => vi.fn()); -const chargeCompletedCallRecordingMock = vi.hoisted(() => vi.fn()); - -vi.mock('src/logic-functions/recall-api/get-recall-bot.util', () => ({ - getRecallBot: getRecallBotMock, -})); - -vi.mock('src/logic-functions/recall-api/list-recall-transcripts.util', () => ({ - listRecallTranscripts: listRecallTranscriptsMock, -})); - -vi.mock( - 'src/logic-functions/recall-api/create-async-recall-transcript.util', - () => ({ - createAsyncRecallTranscript: createAsyncRecallTranscriptMock, - }), -); - -vi.mock('src/logic-functions/flows/download-transcript.util', () => ({ - downloadTranscript: downloadTranscriptMock, -})); - -vi.mock('src/logic-functions/flows/ingest-call-recording-media.util', () => ({ - ingestCallRecordingMedia: ingestCallRecordingMediaMock, -})); - -vi.mock( - 'src/logic-functions/flows/charge-completed-call-recording.util', - () => ({ - chargeCompletedCallRecording: chargeCompletedCallRecordingMock, - }), -); - -const NOW = new Date('2026-06-10T12:00:00.000Z'); - -type CallRecordingNode = Record; - -class FakeCoreApiClient { - mutations: Array<{ id: string; data: Record }> = []; - - constructor(private callRecordingNodes: CallRecordingNode[]) {} - - async query(_query: any): Promise { - return { - callRecordings: { - pageInfo: { hasNextPage: false, endCursor: undefined }, - edges: this.callRecordingNodes.map((node) => ({ node })), - }, - }; - } - - async mutation(mutation: any): Promise { - if (mutation.updateCallRecordings !== undefined) { - const { filter, data } = mutation.updateCallRecordings.__args; - const id = filter.id.eq; - - this.mutations.push({ id, data }); - - return { updateCallRecordings: [{ id }] }; - } - - const { id, data } = mutation.updateCallRecording.__args; - - this.mutations.push({ id, data }); - - return { updateCallRecording: { id } }; - } -} - -const buildClient = (callRecordingNodes: CallRecordingNode[]) => - new FakeCoreApiClient(callRecordingNodes); - -const buildStuckRecordingNode = ( - overrides: CallRecordingNode = {}, -): CallRecordingNode => ({ - id: 'call-recording-1', - status: 'RECORDING', - startedAt: null, - endedAt: null, - externalBotId: 'recall-bot-1', - externalRecordingId: null, - transcript: null, - audio: null, - video: null, - createdAt: '2026-06-09T12:00:00.000Z', - calendarEvent: { - startsAt: '2026-06-09T12:00:00.000Z', - endsAt: '2026-06-09T13:00:00.000Z', - }, - ...overrides, -}); - -describe('convergeDivergedCallRecordings', () => { - beforeEach(() => { - vi.spyOn(console, 'warn').mockImplementation(() => {}); - getRecallBotMock.mockReset(); - listRecallTranscriptsMock.mockReset(); - listRecallTranscriptsMock.mockResolvedValue({ - ok: true, - transcripts: [], - }); - createAsyncRecallTranscriptMock.mockReset(); - createAsyncRecallTranscriptMock.mockResolvedValue({ - ok: true, - transcriptId: 'recall-transcript-1', - }); - downloadTranscriptMock.mockReset(); - downloadTranscriptMock.mockResolvedValue({ outcome: 'pending' }); - ingestCallRecordingMediaMock.mockReset(); - ingestCallRecordingMediaMock.mockResolvedValue({}); - chargeCompletedCallRecordingMock.mockReset(); - chargeCompletedCallRecordingMock.mockResolvedValue(undefined); - }); - - it('heals a stuck RECORDING record from the Recall bot state', async () => { - getRecallBotMock.mockResolvedValue({ - ok: true, - bot: { - status_changes: [ - { code: 'in_call_recording', created_at: '2026-06-09T13:02:30.000Z' }, - { code: 'call_ended', created_at: '2026-06-09T14:00:30.000Z' }, - { code: 'done', created_at: '2026-06-09T14:05:00.000Z' }, - ], - recordings: [ - { - id: 'recall-recording-1', - started_at: '2026-06-09T13:02:00.000Z', - completed_at: '2026-06-09T14:00:00.000Z', - }, - ], - }, - }); - const client = buildClient([buildStuckRecordingNode()]); - - const result = await convergeDivergedCallRecordings({ - client: client as unknown as CoreApiClient, - now: NOW, - }); - - expect(getRecallBotMock).toHaveBeenCalledWith({ - externalBotId: 'recall-bot-1', - }); - expect(ingestCallRecordingMediaMock).toHaveBeenCalledWith({ - callRecordingId: 'call-recording-1', - externalRecordingId: 'recall-recording-1', - hasAudio: false, - hasVideo: false, - }); - expect(listRecallTranscriptsMock).toHaveBeenCalledWith({ - externalRecordingId: 'recall-recording-1', - }); - expect(createAsyncRecallTranscriptMock).toHaveBeenCalledWith({ - externalRecordingId: 'recall-recording-1', - callRecordingId: 'call-recording-1', - }); - expect(client.mutations).toEqual([ - expect.objectContaining({ - id: 'call-recording-1', - data: expect.objectContaining({ - status: 'PROCESSING', - startedAt: '2026-06-09T13:02:00.000Z', - endedAt: '2026-06-09T14:00:00.000Z', - externalRecordingId: 'recall-recording-1', - }), - }), - ]); - expect(chargeCompletedCallRecordingMock).not.toHaveBeenCalled(); - expect(result).toEqual({ - candidateCount: 1, - updatedCallRecordingIds: ['call-recording-1'], - markedFailedCallRecordingIds: [], - requestedTranscriptCallRecordingIds: ['call-recording-1'], - unconvergeableCallRecordingIds: [], - skippedNotStartedCallRecordingIds: [], - }); - }); - - it('marks FAILED when Recall is done but has no recording artifact path', async () => { - getRecallBotMock.mockResolvedValue({ - ok: true, - bot: { - status_changes: [ - { code: 'done', created_at: '2026-06-09T14:05:00.000Z' }, - ], - recordings: [], - }, - }); - const client = buildClient([buildStuckRecordingNode()]); - - const result = await convergeDivergedCallRecordings({ - client: client as unknown as CoreApiClient, - now: NOW, - }); - - expect(listRecallTranscriptsMock).not.toHaveBeenCalled(); - expect(ingestCallRecordingMediaMock).not.toHaveBeenCalled(); - expect(client.mutations).toEqual([ - { - id: 'call-recording-1', - data: { - status: 'FAILED', - meetingBotFailureReason: 'recording_artifacts_unavailable', - }, - }, - ]); - expect(result.updatedCallRecordingIds).toEqual(['call-recording-1']); - }); - - it('completes and charges when convergence lands the last artifact', async () => { - getRecallBotMock.mockResolvedValue({ - ok: true, - bot: { - status_changes: [ - { code: 'done', created_at: '2026-06-09T14:05:00.000Z' }, - ], - recordings: [ - { - id: 'recall-recording-1', - started_at: '2026-06-09T13:02:00.000Z', - completed_at: '2026-06-09T14:00:00.000Z', - }, - ], - }, - }); - ingestCallRecordingMediaMock.mockResolvedValue({ - audio: [{ fileId: 'file-audio-1', label: 'audio.mp3' }], - video: [{ fileId: 'file-video-1', label: 'video.mp4' }], - }); - const client = buildClient([ - buildStuckRecordingNode({ - status: 'PROCESSING', - startedAt: '2026-06-09T13:02:00.000Z', - endedAt: '2026-06-09T14:00:00.000Z', - externalRecordingId: 'recall-recording-1', - transcript: [{ participant: { id: 1 }, words: [] }], - }), - ]); - - const result = await convergeDivergedCallRecordings({ - client: client as unknown as CoreApiClient, - now: NOW, - }); - - expect(createAsyncRecallTranscriptMock).not.toHaveBeenCalled(); - expect(listRecallTranscriptsMock).not.toHaveBeenCalled(); - expect(client.mutations).toEqual([ - { - id: 'call-recording-1', - data: { - audio: [{ fileId: 'file-audio-1', label: 'audio.mp3' }], - video: [{ fileId: 'file-video-1', label: 'video.mp4' }], - }, - }, - { - id: 'call-recording-1', - data: { status: 'COMPLETED' }, - }, - ]); - expect(chargeCompletedCallRecordingMock).toHaveBeenCalledWith({ - callRecordingId: 'call-recording-1', - startedAt: '2026-06-09T13:02:00.000Z', - endedAt: '2026-06-09T14:00:00.000Z', - }); - expect(result).toEqual({ - candidateCount: 1, - updatedCallRecordingIds: ['call-recording-1'], - markedFailedCallRecordingIds: [], - requestedTranscriptCallRecordingIds: [], - unconvergeableCallRecordingIds: [], - skippedNotStartedCallRecordingIds: [], - }); - }); - - it('skips records whose meeting has not started yet', async () => { - const client = buildClient([ - buildStuckRecordingNode({ - calendarEvent: { - startsAt: '2026-06-10T12:30:00.000Z', - endsAt: '2026-06-10T13:30:00.000Z', - }, - }), - ]); - - const result = await convergeDivergedCallRecordings({ - client: client as unknown as CoreApiClient, - now: NOW, - }); - - expect(getRecallBotMock).not.toHaveBeenCalled(); - expect(client.mutations).toEqual([]); - expect(result.skippedNotStartedCallRecordingIds).toEqual([ - 'call-recording-1', - ]); - }); - - it('converges a meeting that ended early while its scheduled end is still in the future', async () => { - getRecallBotMock.mockResolvedValue({ - ok: true, - bot: { - status_changes: [ - { code: 'done', created_at: '2026-06-10T11:30:00.000Z' }, - ], - recordings: [ - { - id: 'recall-recording-1', - started_at: '2026-06-10T11:05:00.000Z', - completed_at: '2026-06-10T11:25:00.000Z', - }, - ], - }, - }); - const client = buildClient([ - buildStuckRecordingNode({ - calendarEvent: { - startsAt: '2026-06-10T11:00:00.000Z', - endsAt: '2026-06-10T13:00:00.000Z', - }, - }), - ]); - - const result = await convergeDivergedCallRecordings({ - client: client as unknown as CoreApiClient, - now: NOW, - }); - - expect(getRecallBotMock).toHaveBeenCalledWith({ - externalBotId: 'recall-bot-1', - }); - expect(result.updatedCallRecordingIds).toEqual(['call-recording-1']); - expect(result.skippedNotStartedCallRecordingIds).toEqual([]); - }); - - it('marks FAILED without clearing the bot id when Recall returns 404', async () => { - getRecallBotMock.mockResolvedValue({ - ok: false, - status: 404, - errorMessage: 'Recall API responded with HTTP 404', - }); - const client = buildClient([buildStuckRecordingNode()]); - - const result = await convergeDivergedCallRecordings({ - client: client as unknown as CoreApiClient, - now: NOW, - }); - - expect(client.mutations).toEqual([ - { - id: 'call-recording-1', - data: { - status: 'FAILED', - meetingBotFailureReason: 'recall_bot_not_found', - }, - }, - ]); - expect(result.markedFailedCallRecordingIds).toEqual(['call-recording-1']); - expect(console.warn).toHaveBeenCalled(); - }); - - it('does not downgrade a COMPLETED record when its bot 404s', async () => { - getRecallBotMock.mockResolvedValue({ - ok: false, - status: 404, - errorMessage: 'Recall API responded with HTTP 404', - }); - const client = buildClient([ - buildStuckRecordingNode({ - status: 'COMPLETED', - startedAt: '2026-06-09T13:02:00.000Z', - transcript: [{ participant: { id: 1 }, words: [] }], - }), - ]); - - const result = await convergeDivergedCallRecordings({ - client: client as unknown as CoreApiClient, - now: NOW, - }); - - expect(client.mutations).toEqual([]); - expect(result.unconvergeableCallRecordingIds).toEqual(['call-recording-1']); - }); - - it('logs candidates whose meeting ended before the lookback bound instead of converging them', async () => { - const client = buildClient([ - buildStuckRecordingNode({ - calendarEvent: { endsAt: '2026-06-01T13:00:00.000Z' }, - }), - ]); - - const result = await convergeDivergedCallRecordings({ - client: client as unknown as CoreApiClient, - now: NOW, - }); - - expect(getRecallBotMock).not.toHaveBeenCalled(); - expect(client.mutations).toEqual([]); - expect(result.unconvergeableCallRecordingIds).toEqual(['call-recording-1']); - expect(console.warn).toHaveBeenCalled(); - }); - - it('converges candidates created long before a recently ended meeting', async () => { - getRecallBotMock.mockResolvedValue({ - ok: true, - bot: { - status_changes: [ - { code: 'in_call_recording', created_at: '2026-06-09T13:02:00.000Z' }, - ], - }, - }); - const client = buildClient([ - buildStuckRecordingNode({ - createdAt: '2026-06-01T12:00:00.000Z', - startedAt: '2026-06-09T13:02:00.000Z', - }), - ]); - - const result = await convergeDivergedCallRecordings({ - client: client as unknown as CoreApiClient, - now: NOW, - }); - - expect(getRecallBotMock).toHaveBeenCalledWith({ - externalBotId: 'recall-bot-1', - }); - expect(result.unconvergeableCallRecordingIds).toEqual([]); - }); - - it('applies the downgrade guard to pulled statuses while still filling timestamps', async () => { - getRecallBotMock.mockResolvedValue({ - ok: true, - bot: { - status_changes: [ - { code: 'in_call_recording', created_at: '2026-06-09T13:02:00.000Z' }, - ], - recordings: [ - { id: 'recall-recording-1', started_at: '2026-06-09T13:02:00.000Z' }, - ], - }, - }); - const client = buildClient([ - buildStuckRecordingNode({ status: 'PROCESSING' }), - ]); - - await convergeDivergedCallRecordings({ - client: client as unknown as CoreApiClient, - now: NOW, - }); - - expect(client.mutations).toEqual([ - { - id: 'call-recording-1', - data: { - startedAt: '2026-06-09T13:02:00.000Z', - externalRecordingId: 'recall-recording-1', - }, - }, - ]); - }); - - it('requests a transcript for a COMPLETED candidate that has none', async () => { - getRecallBotMock.mockResolvedValue({ - ok: true, - bot: { - status_changes: [ - { code: 'done', created_at: '2026-06-09T14:05:00.000Z' }, - ], - recordings: [ - { - id: 'recall-recording-1', - started_at: '2026-06-09T13:02:00.000Z', - completed_at: '2026-06-09T14:00:00.000Z', - }, - ], - }, - }); - const client = buildClient([ - buildStuckRecordingNode({ - status: 'COMPLETED', - startedAt: '2026-06-09T13:02:00.000Z', - externalRecordingId: 'recall-recording-1', - }), - ]); - - const result = await convergeDivergedCallRecordings({ - client: client as unknown as CoreApiClient, - now: NOW, - }); - - expect(createAsyncRecallTranscriptMock).toHaveBeenCalledTimes(1); - expect(createAsyncRecallTranscriptMock).toHaveBeenCalledWith({ - externalRecordingId: 'recall-recording-1', - callRecordingId: 'call-recording-1', - }); - expect(client.mutations).toEqual([ - { - id: 'call-recording-1', - data: { - endedAt: '2026-06-09T14:00:00.000Z', - transcript: { - recallTranscriptId: 'recall-transcript-1', - status: 'PENDING', - requestedAt: NOW.toISOString(), - }, - }, - }, - ]); - expect(result.requestedTranscriptCallRecordingIds).toEqual([ - 'call-recording-1', - ]); - }); - - it('does not create a duplicate transcript when Recall already has one processing', async () => { - getRecallBotMock.mockResolvedValue({ - ok: true, - bot: { - status_changes: [ - { code: 'done', created_at: '2026-06-09T14:05:00.000Z' }, - ], - recordings: [ - { - id: 'recall-recording-1', - started_at: '2026-06-09T13:02:00.000Z', - completed_at: '2026-06-09T14:00:00.000Z', - }, - ], - }, - }); - listRecallTranscriptsMock.mockResolvedValue({ - ok: true, - transcripts: [ - { - id: 'recall-transcript-1', - statusCode: 'processing', - statusSubCode: undefined, - }, - ], - }); - const client = buildClient([buildStuckRecordingNode()]); - - const result = await convergeDivergedCallRecordings({ - client: client as unknown as CoreApiClient, - now: NOW, - }); - - expect(createAsyncRecallTranscriptMock).not.toHaveBeenCalled(); - expect(downloadTranscriptMock).not.toHaveBeenCalled(); - expect(client.mutations).toEqual([ - { - id: 'call-recording-1', - data: { - status: 'PROCESSING', - startedAt: '2026-06-09T13:02:00.000Z', - endedAt: '2026-06-09T14:00:00.000Z', - externalRecordingId: 'recall-recording-1', - }, - }, - ]); - expect(result.requestedTranscriptCallRecordingIds).toEqual([]); - }); - - it('fills a completed Recall transcript artifact during convergence', async () => { - const transcriptContent = [ - { - participant: { id: 1, name: 'Ada' }, - words: [{ text: 'hello', start_timestamp: 1, end_timestamp: 2 }], - }, - ]; - - getRecallBotMock.mockResolvedValue({ - ok: true, - bot: { - status_changes: [ - { code: 'done', created_at: '2026-06-09T14:05:00.000Z' }, - ], - recordings: [ - { - id: 'recall-recording-1', - started_at: '2026-06-09T13:02:00.000Z', - completed_at: '2026-06-09T14:00:00.000Z', - }, - ], - }, - }); - listRecallTranscriptsMock.mockResolvedValue({ - ok: true, - transcripts: [ - { - id: 'recall-transcript-1', - statusCode: 'done', - statusSubCode: undefined, - }, - ], - }); - downloadTranscriptMock.mockResolvedValue({ - outcome: 'filled', - content: transcriptContent, - }); - const client = buildClient([ - buildStuckRecordingNode({ - status: 'PROCESSING', - startedAt: '2026-06-09T13:02:00.000Z', - endedAt: '2026-06-09T14:00:00.000Z', - externalRecordingId: 'recall-recording-1', - transcript: { - recallTranscriptId: 'legacy-pending-transcript', - status: 'PENDING', - requestedAt: '2026-06-09T14:05:30.000Z', - }, - audio: [{ fileId: 'file-audio-1', label: 'audio.mp3' }], - video: [{ fileId: 'file-video-1', label: 'video.mp4' }], - }), - ]); - - const result = await convergeDivergedCallRecordings({ - client: client as unknown as CoreApiClient, - now: NOW, - }); - - expect(createAsyncRecallTranscriptMock).not.toHaveBeenCalled(); - expect(downloadTranscriptMock).toHaveBeenCalledWith({ - transcriptId: 'recall-transcript-1', - }); - expect(client.mutations).toEqual([ - { - id: 'call-recording-1', - data: { transcript: transcriptContent }, - }, - { - id: 'call-recording-1', - data: { status: 'COMPLETED' }, - }, - ]); - expect(chargeCompletedCallRecordingMock).toHaveBeenCalledWith({ - callRecordingId: 'call-recording-1', - startedAt: '2026-06-09T13:02:00.000Z', - endedAt: '2026-06-09T14:00:00.000Z', - }); - expect(result.requestedTranscriptCallRecordingIds).toEqual([]); - }); - - it('marks the call recording failed when Recall has a failed transcript artifact', async () => { - getRecallBotMock.mockResolvedValue({ - ok: true, - bot: { - status_changes: [ - { code: 'done', created_at: '2026-06-09T14:05:00.000Z' }, - ], - recordings: [ - { - id: 'recall-recording-1', - started_at: '2026-06-09T13:02:00.000Z', - completed_at: '2026-06-09T14:00:00.000Z', - }, - ], - }, - }); - listRecallTranscriptsMock.mockResolvedValue({ - ok: true, - transcripts: [ - { - id: 'recall-transcript-1', - statusCode: 'failed', - statusSubCode: 'audio_missing', - }, - ], - }); - const client = buildClient([ - buildStuckRecordingNode({ - status: 'PROCESSING', - startedAt: '2026-06-09T13:02:00.000Z', - endedAt: '2026-06-09T14:00:00.000Z', - externalRecordingId: 'recall-recording-1', - }), - ]); - - const result = await convergeDivergedCallRecordings({ - client: client as unknown as CoreApiClient, - now: NOW, - }); - - expect(createAsyncRecallTranscriptMock).not.toHaveBeenCalled(); - expect(downloadTranscriptMock).not.toHaveBeenCalled(); - expect(client.mutations).toEqual([ - { - id: 'call-recording-1', - data: { - status: 'FAILED', - transcript: { - recallTranscriptId: 'recall-transcript-1', - status: 'FAILED', - subCode: 'audio_missing', - }, - meetingBotFailureReason: 'transcript_failed:audio_missing', - }, - }, - ]); - expect(result.requestedTranscriptCallRecordingIds).toEqual([]); - }); - - it('does not mutate a record the bot state agrees with', async () => { - getRecallBotMock.mockResolvedValue({ - ok: true, - bot: { - status_changes: [ - { code: 'in_call_recording', created_at: '2026-06-09T13:02:00.000Z' }, - ], - }, - }); - const client = buildClient([ - buildStuckRecordingNode({ startedAt: '2026-06-09T13:02:00.000Z' }), - ]); - - const result = await convergeDivergedCallRecordings({ - client: client as unknown as CoreApiClient, - now: NOW, - }); - - expect(client.mutations).toEqual([]); - expect(result.updatedCallRecordingIds).toEqual([]); - }); -}); diff --git a/packages/twenty-apps/public/twenty-meeting-bot/src/logic-functions/flows/__tests__/download-transcript.test.ts b/packages/twenty-apps/public/twenty-meeting-bot/src/logic-functions/flows/__tests__/download-transcript.test.ts deleted file mode 100644 index bc1b7f0ee3af4..0000000000000 --- a/packages/twenty-apps/public/twenty-meeting-bot/src/logic-functions/flows/__tests__/download-transcript.test.ts +++ /dev/null @@ -1,74 +0,0 @@ -import { beforeEach, describe, expect, it, vi } from 'vitest'; - -import { downloadTranscript } from 'src/logic-functions/flows/download-transcript.util'; - -const retrieveRecallTranscriptMock = vi.hoisted(() => vi.fn()); - -vi.mock( - 'src/logic-functions/recall-api/retrieve-recall-transcript.util', - () => ({ - retrieveRecallTranscript: retrieveRecallTranscriptMock, - }), -); - -describe('downloadTranscript', () => { - const fetchMock = vi.fn(); - - beforeEach(() => { - vi.spyOn(console, 'warn').mockImplementation(() => {}); - retrieveRecallTranscriptMock.mockReset(); - fetchMock.mockReset(); - vi.stubGlobal('fetch', fetchMock); - }); - - it('downloads transcript content with a timeout', async () => { - const transcriptContent = [{ participant: { id: 1 }, words: [] }]; - - retrieveRecallTranscriptMock.mockResolvedValue({ - ok: true, - transcript: { - downloadUrl: 'https://recall-transcripts.example.com/transcript-1', - statusCode: 'done', - statusSubCode: undefined, - }, - }); - fetchMock.mockResolvedValue({ - ok: true, - json: async () => transcriptContent, - }); - - const result = await downloadTranscript({ - transcriptId: 'recall-transcript-1', - }); - - expect(result).toEqual({ outcome: 'filled', content: transcriptContent }); - expect(fetchMock).toHaveBeenCalledWith( - 'https://recall-transcripts.example.com/transcript-1', - expect.objectContaining({ - signal: expect.any(AbortSignal), - }), - ); - }); - - it('logs raw download failures but returns a generic error', async () => { - retrieveRecallTranscriptMock.mockResolvedValue({ - ok: true, - transcript: { - downloadUrl: 'https://recall-transcripts.example.com/transcript-1', - statusCode: 'done', - statusSubCode: undefined, - }, - }); - fetchMock.mockRejectedValue(new Error('socket leaked detail')); - - await expect( - downloadTranscript({ transcriptId: 'recall-transcript-1' }), - ).resolves.toEqual({ - outcome: 'error', - errorMessage: 'transcript download failed', - }); - expect(console.warn).toHaveBeenCalledWith( - expect.stringContaining('socket leaked detail'), - ); - }); -}); diff --git a/packages/twenty-apps/public/twenty-meeting-bot/src/logic-functions/flows/__tests__/handle-recall-webhook.test.ts b/packages/twenty-apps/public/twenty-meeting-bot/src/logic-functions/flows/__tests__/handle-recall-webhook.test.ts deleted file mode 100644 index ae2e85b9aaead..0000000000000 --- a/packages/twenty-apps/public/twenty-meeting-bot/src/logic-functions/flows/__tests__/handle-recall-webhook.test.ts +++ /dev/null @@ -1,1286 +0,0 @@ -import { type CoreApiClient } from 'twenty-client-sdk/core'; -import { beforeEach, describe, expect, it, vi } from 'vitest'; - -import { handleRecallWebhook } from 'src/logic-functions/flows/handle-recall-webhook.util'; - -const WORKSPACE_ID = '123e4567-e89b-12d3-a456-426614174000'; - -const buildRecordingDoneWebhookBody = () => ({ - event: 'recording.done', - data: { - bot: { - id: 'recall-bot-1', - metadata: { - twentyWorkspaceId: WORKSPACE_ID, - }, - }, - recording: { - id: 'recall-recording-1', - }, - }, -}); - -const getRecallBotMock = vi.hoisted(() => vi.fn()); -const listRecallTranscriptsMock = vi.hoisted(() => vi.fn()); -const createAsyncRecallTranscriptMock = vi.hoisted(() => vi.fn()); -const retrieveRecallTranscriptMock = vi.hoisted(() => vi.fn()); -const ingestCallRecordingMediaMock = vi.hoisted(() => vi.fn()); -const chargeCompletedCallRecordingMock = vi.hoisted(() => vi.fn()); - -vi.mock('src/logic-functions/recall-api/get-recall-bot.util', () => ({ - getRecallBot: getRecallBotMock, -})); - -vi.mock('src/logic-functions/recall-api/list-recall-transcripts.util', () => ({ - listRecallTranscripts: listRecallTranscriptsMock, -})); - -vi.mock( - 'src/logic-functions/recall-api/create-async-recall-transcript.util', - () => ({ - createAsyncRecallTranscript: createAsyncRecallTranscriptMock, - }), -); - -vi.mock( - 'src/logic-functions/recall-api/retrieve-recall-transcript.util', - () => ({ - retrieveRecallTranscript: retrieveRecallTranscriptMock, - }), -); - -vi.mock('src/logic-functions/flows/ingest-call-recording-media.util', () => ({ - ingestCallRecordingMedia: ingestCallRecordingMediaMock, -})); - -vi.mock( - 'src/logic-functions/flows/charge-completed-call-recording.util', - () => ({ - chargeCompletedCallRecording: chargeCompletedCallRecordingMock, - }), -); - -type CallRecordingNode = { - id: string; - status?: string | null; - externalBotId?: string | null; - externalRecordingId?: string | null; - startedAt?: string | null; - endedAt?: string | null; - transcript?: unknown; - audio?: unknown; - video?: unknown; -}; - -class FakeCoreApiClient { - callRecordings: CallRecordingNode[]; - mutations: Array<{ id: string; data: Record }> = []; - - constructor(callRecordings: CallRecordingNode[]) { - this.callRecordings = callRecordings; - } - - async query(query: any): Promise { - if (query.callRecordings !== undefined) { - const filter = query.callRecordings.__args.filter; - - return { - callRecordings: { - edges: this.filterCallRecordings(filter).map((callRecording) => ({ - node: callRecording, - })), - }, - }; - } - - throw new Error(`Unhandled query: ${JSON.stringify(query)}`); - } - - async mutation(mutation: any): Promise { - if (mutation.updateCallRecordings !== undefined) { - const { filter, data } = mutation.updateCallRecordings.__args; - const id = filter.id.eq; - - this.mutations.push({ id, data }); - - return { updateCallRecordings: [{ id }] }; - } - - if (mutation.updateCallRecording !== undefined) { - const { id, data } = mutation.updateCallRecording.__args; - - this.mutations.push({ id, data }); - - return { - updateCallRecording: { - id, - }, - }; - } - - throw new Error(`Unhandled mutation: ${JSON.stringify(mutation)}`); - } - - private filterCallRecordings(filter: any): CallRecordingNode[] { - if (filter.id?.eq !== undefined) { - return this.callRecordings.filter( - (callRecording) => callRecording.id === filter.id.eq, - ); - } - - if (filter.externalBotId?.eq !== undefined) { - return this.callRecordings.filter( - (callRecording) => - callRecording.externalBotId === filter.externalBotId.eq, - ); - } - - throw new Error( - `Unhandled call recording filter: ${JSON.stringify(filter)}`, - ); - } -} - -describe('handleRecallWebhook', () => { - beforeEach(() => { - vi.spyOn(console, 'warn').mockImplementation(() => {}); - getRecallBotMock.mockReset(); - getRecallBotMock.mockResolvedValue({ - ok: false, - status: null, - errorMessage: 'bot fetch disabled in test', - }); - listRecallTranscriptsMock.mockReset(); - listRecallTranscriptsMock.mockResolvedValue({ - ok: true, - transcripts: [], - }); - createAsyncRecallTranscriptMock.mockReset(); - createAsyncRecallTranscriptMock.mockResolvedValue({ - ok: false, - status: null, - errorMessage: 'transcript request disabled in test', - }); - retrieveRecallTranscriptMock.mockReset(); - retrieveRecallTranscriptMock.mockResolvedValue({ - ok: false, - status: null, - errorMessage: 'transcript retrieval disabled in test', - }); - ingestCallRecordingMediaMock.mockReset(); - ingestCallRecordingMediaMock.mockResolvedValue({}); - chargeCompletedCallRecordingMock.mockReset(); - chargeCompletedCallRecordingMock.mockResolvedValue(undefined); - }); - - it('updates a call recording from bot metadata on status change events', async () => { - const client = new FakeCoreApiClient([ - { - id: 'call-recording-1', - status: 'JOINING', - externalBotId: 'recall-bot-1', - }, - ]); - - const result = await handleRecallWebhook({ - client: client as unknown as CoreApiClient, - body: { - event: 'bot.status_change', - data: { - bot: { - id: 'recall-bot-1', - metadata: { - twentyCallRecordingId: 'call-recording-1', - }, - }, - status: { - code: 'in_call_recording', - }, - }, - }, - }); - - expect(result).toEqual({ - status: 'updated', - event: 'bot.status_change', - callRecordingId: 'call-recording-1', - callRecordingStatus: 'RECORDING', - }); - expect(client.mutations).toEqual([ - { - id: 'call-recording-1', - data: { - status: 'RECORDING', - externalBotId: 'recall-bot-1', - }, - }, - ]); - }); - - it('reads bot metadata nested under data when a top-level bot has none', async () => { - const client = new FakeCoreApiClient([ - { - id: 'call-recording-1', - status: 'JOINING', - externalBotId: 'recall-bot-1', - }, - ]); - - const result = await handleRecallWebhook({ - client: client as unknown as CoreApiClient, - body: { - event: 'bot.status_change', - bot: { - id: 'recall-bot-1', - }, - data: { - bot: { - id: 'recall-bot-1', - metadata: { - twentyCallRecordingId: 'call-recording-1', - }, - }, - status: { - code: 'in_call_recording', - }, - }, - }, - }); - - expect(result).toEqual({ - status: 'updated', - event: 'bot.status_change', - callRecordingId: 'call-recording-1', - callRecordingStatus: 'RECORDING', - }); - }); - - it('matches by metadata id when the recording carries no external bot id', async () => { - const client = new FakeCoreApiClient([ - { - id: 'call-recording-1', - status: 'SCHEDULED', - externalBotId: null, - }, - ]); - - const result = await handleRecallWebhook({ - client: client as unknown as CoreApiClient, - body: { - event: 'bot.status_change', - data: { - bot: { - id: 'recall-bot-1', - metadata: { - twentyCallRecordingId: 'call-recording-1', - }, - }, - status: { - code: 'in_call_recording', - }, - }, - }, - }); - - expect(result).toEqual({ - status: 'updated', - event: 'bot.status_change', - callRecordingId: 'call-recording-1', - callRecordingStatus: 'RECORDING', - }); - expect(client.mutations).toEqual([ - { - id: 'call-recording-1', - data: { - status: 'RECORDING', - externalBotId: 'recall-bot-1', - }, - }, - ]); - }); - - it('prefers the metadata id over a different recording carrying the bot id', async () => { - const client = new FakeCoreApiClient([ - { - id: 'call-recording-stale', - status: 'SCHEDULED', - externalBotId: 'recall-bot-1', - }, - { - id: 'call-recording-current', - status: 'SCHEDULED', - externalBotId: null, - }, - ]); - - const result = await handleRecallWebhook({ - client: client as unknown as CoreApiClient, - body: { - event: 'bot.status_change', - data: { - bot: { - id: 'recall-bot-1', - metadata: { - twentyCallRecordingId: 'call-recording-current', - }, - }, - status: { - code: 'in_call_recording', - }, - }, - }, - }); - - expect(result).toEqual({ - status: 'updated', - event: 'bot.status_change', - callRecordingId: 'call-recording-current', - callRecordingStatus: 'RECORDING', - }); - expect(client.mutations).toEqual([ - { - id: 'call-recording-current', - data: { - status: 'RECORDING', - externalBotId: 'recall-bot-1', - }, - }, - ]); - }); - - it('falls back to external bot id matching when call recording metadata is absent', async () => { - const client = new FakeCoreApiClient([ - { - id: 'call-recording-1', - status: 'PROCESSING', - externalBotId: 'recall-bot-1', - }, - ]); - - const result = await handleRecallWebhook({ - client: client as unknown as CoreApiClient, - body: { - event: 'recording.done', - data: { - bot: { - id: 'recall-bot-1', - metadata: { - twentyWorkspaceId: WORKSPACE_ID, - }, - }, - recording: { - id: 'recall-recording-1', - }, - }, - }, - }); - - expect(result).toEqual({ - status: 'updated', - event: 'recording.done', - callRecordingId: 'call-recording-1', - callRecordingStatus: 'PROCESSING', - }); - expect(client.mutations).toEqual([ - { - id: 'call-recording-1', - data: { - status: 'PROCESSING', - externalBotId: 'recall-bot-1', - externalRecordingId: 'recall-recording-1', - }, - }, - ]); - }); - - it('fills startedAt from the status timestamp when the bot starts recording', async () => { - const client = new FakeCoreApiClient([ - { - id: 'call-recording-1', - status: 'JOINING', - externalBotId: 'recall-bot-1', - }, - ]); - - const result = await handleRecallWebhook({ - client: client as unknown as CoreApiClient, - body: { - event: 'bot.status_change', - data: { - bot: { - id: 'recall-bot-1', - metadata: { - twentyCallRecordingId: 'call-recording-1', - }, - }, - status: { - code: 'in_call_recording', - created_at: '2026-01-01T13:02:00.000Z', - }, - }, - }, - }); - - expect(result).toEqual({ - status: 'updated', - event: 'bot.status_change', - callRecordingId: 'call-recording-1', - callRecordingStatus: 'RECORDING', - }); - expect(client.mutations).toEqual([ - { - id: 'call-recording-1', - data: { - status: 'RECORDING', - externalBotId: 'recall-bot-1', - startedAt: '2026-01-01T13:02:00.000Z', - }, - }, - ]); - }); - - it('fills endedAt from the status timestamp when the recording is done', async () => { - const client = new FakeCoreApiClient([ - { - id: 'call-recording-1', - status: 'PROCESSING', - externalBotId: 'recall-bot-1', - startedAt: '2026-01-01T13:02:00.000Z', - }, - ]); - - const result = await handleRecallWebhook({ - client: client as unknown as CoreApiClient, - body: { - event: 'bot.status_change', - data: { - bot: { - id: 'recall-bot-1', - metadata: { - twentyCallRecordingId: 'call-recording-1', - }, - }, - status: { - code: 'done', - created_at: '2026-01-01T14:05:00.000Z', - }, - }, - }, - }); - - expect(result).toEqual({ - status: 'updated', - event: 'bot.status_change', - callRecordingId: 'call-recording-1', - callRecordingStatus: 'PROCESSING', - }); - expect(client.mutations).toEqual([ - { - id: 'call-recording-1', - data: { - status: 'PROCESSING', - externalBotId: 'recall-bot-1', - endedAt: '2026-01-01T14:05:00.000Z', - }, - }, - ]); - }); - - it('normalizes microsecond-precision Recall timestamps before writing them', async () => { - const client = new FakeCoreApiClient([ - { - id: 'call-recording-1', - status: 'PROCESSING', - externalBotId: 'recall-bot-1', - startedAt: '2026-06-10T11:02:00.000Z', - }, - ]); - - await handleRecallWebhook({ - client: client as unknown as CoreApiClient, - body: { - event: 'bot.status_change', - data: { - bot: { - id: 'recall-bot-1', - metadata: { - twentyCallRecordingId: 'call-recording-1', - }, - }, - status: { - code: 'done', - created_at: '2026-06-10T12:17:28.281597+00:00', - }, - }, - }, - }); - - expect(client.mutations).toEqual([ - { - id: 'call-recording-1', - data: { - status: 'PROCESSING', - externalBotId: 'recall-bot-1', - endedAt: '2026-06-10T12:17:28.281Z', - }, - }, - ]); - }); - - it('does not overwrite an already-set startedAt on a redelivered recording event', async () => { - const client = new FakeCoreApiClient([ - { - id: 'call-recording-1', - status: 'RECORDING', - externalBotId: 'recall-bot-1', - startedAt: '2026-01-01T13:02:00.000Z', - }, - ]); - - await handleRecallWebhook({ - client: client as unknown as CoreApiClient, - body: { - event: 'bot.status_change', - data: { - bot: { - id: 'recall-bot-1', - metadata: { - twentyCallRecordingId: 'call-recording-1', - }, - }, - status: { - code: 'in_call_recording', - created_at: '2026-01-01T13:09:00.000Z', - }, - }, - }, - }); - - expect(client.mutations).toEqual([ - { - id: 'call-recording-1', - data: { - status: 'RECORDING', - externalBotId: 'recall-bot-1', - }, - }, - ]); - }); - - it('does not overwrite an already-set endedAt on a redelivered done event', async () => { - const client = new FakeCoreApiClient([ - { - id: 'call-recording-1', - status: 'PROCESSING', - externalBotId: 'recall-bot-1', - startedAt: '2026-01-01T13:02:00.000Z', - endedAt: '2026-01-01T14:05:00.000Z', - transcript: { - recallTranscriptId: 'recall-transcript-1', - status: 'PENDING', - requestedAt: '2026-01-01T14:06:00.000Z', - }, - }, - ]); - - await handleRecallWebhook({ - client: client as unknown as CoreApiClient, - body: { - event: 'bot.status_change', - data: { - bot: { - id: 'recall-bot-1', - metadata: { - twentyCallRecordingId: 'call-recording-1', - }, - }, - status: { - code: 'done', - created_at: '2026-01-01T14:11:00.000Z', - }, - }, - }, - }); - - expect(client.mutations).toEqual([ - { - id: 'call-recording-1', - data: { - status: 'PROCESSING', - externalBotId: 'recall-bot-1', - }, - }, - ]); - }); - - it('skips a late done event once the recording is COMPLETED', async () => { - const client = new FakeCoreApiClient([ - { - id: 'call-recording-1', - status: 'COMPLETED', - externalBotId: 'recall-bot-1', - startedAt: '2026-01-01T13:02:00.000Z', - endedAt: '2026-01-01T14:05:00.000Z', - }, - ]); - - const result = await handleRecallWebhook({ - client: client as unknown as CoreApiClient, - body: { - event: 'bot.status_change', - data: { - bot: { - id: 'recall-bot-1', - metadata: { - twentyCallRecordingId: 'call-recording-1', - }, - }, - status: { - code: 'done', - created_at: '2026-01-01T14:11:00.000Z', - }, - }, - }, - }); - - expect(result).toEqual({ - status: 'skipped', - event: 'bot.status_change', - reason: 'stale status event (COMPLETED -> PROCESSING)', - }); - expect(client.mutations).toEqual([]); - }); - - it('skips out-of-order events that would move the status backwards', async () => { - const client = new FakeCoreApiClient([ - { - id: 'call-recording-1', - status: 'COMPLETED', - externalBotId: 'recall-bot-1', - }, - ]); - - const result = await handleRecallWebhook({ - client: client as unknown as CoreApiClient, - body: { - event: 'bot.status_change', - data: { - bot: { - id: 'recall-bot-1', - metadata: { - twentyCallRecordingId: 'call-recording-1', - }, - }, - status: { - code: 'in_call_recording', - }, - }, - }, - }); - - expect(result).toEqual({ - status: 'skipped', - event: 'bot.status_change', - reason: 'stale status event (COMPLETED -> RECORDING)', - }); - expect(client.mutations).toEqual([]); - }); - - it('skips events whose metadata points at a missing call recording', async () => { - const client = new FakeCoreApiClient([]); - - const result = await handleRecallWebhook({ - client: client as unknown as CoreApiClient, - body: { - event: 'bot.status_change', - data: { - bot: { - metadata: { - twentyCallRecordingId: 'call-recording-deleted', - }, - }, - status: { - code: 'in_call_recording', - }, - }, - }, - }); - - expect(result).toEqual({ - status: 'skipped', - event: 'bot.status_change', - reason: 'no matching call recording', - }); - expect(client.mutations).toEqual([]); - }); - - it('skips unsupported events', async () => { - const client = new FakeCoreApiClient([]); - - const result = await handleRecallWebhook({ - client: client as unknown as CoreApiClient, - body: { - event: 'participant_events.done', - data: {}, - }, - }); - - expect(result).toEqual({ - status: 'skipped', - event: 'participant_events.done', - reason: 'unsupported Recall event status participant_events.done', - }); - expect(client.mutations).toEqual([]); - }); - - it('requests a transcript once when the recording first completes', async () => { - createAsyncRecallTranscriptMock.mockResolvedValue({ - ok: true, - transcriptId: 'recall-transcript-1', - }); - const client = new FakeCoreApiClient([ - { - id: 'call-recording-1', - status: 'PROCESSING', - externalBotId: 'recall-bot-1', - transcript: null, - }, - ]); - - await handleRecallWebhook({ - client: client as unknown as CoreApiClient, - body: buildRecordingDoneWebhookBody(), - }); - - expect(createAsyncRecallTranscriptMock).toHaveBeenCalledTimes(1); - expect(createAsyncRecallTranscriptMock).toHaveBeenCalledWith({ - externalRecordingId: 'recall-recording-1', - callRecordingId: 'call-recording-1', - }); - expect(client.mutations).toEqual([ - { - id: 'call-recording-1', - data: { - status: 'PROCESSING', - externalBotId: 'recall-bot-1', - externalRecordingId: 'recall-recording-1', - transcript: { - recallTranscriptId: 'recall-transcript-1', - status: 'PENDING', - requestedAt: expect.any(String), - }, - }, - }, - ]); - }); - - it('does not re-request a transcript on a redelivered done event while Recall list is stale', async () => { - const client = new FakeCoreApiClient([ - { - id: 'call-recording-1', - status: 'PROCESSING', - externalBotId: 'recall-bot-1', - externalRecordingId: 'recall-recording-1', - transcript: { - recallTranscriptId: 'recall-transcript-1', - status: 'PENDING', - requestedAt: '2026-01-01T14:06:00.000Z', - }, - }, - ]); - - await handleRecallWebhook({ - client: client as unknown as CoreApiClient, - body: buildRecordingDoneWebhookBody(), - }); - - expect(createAsyncRecallTranscriptMock).not.toHaveBeenCalled(); - expect(listRecallTranscriptsMock).toHaveBeenCalledWith({ - externalRecordingId: 'recall-recording-1', - }); - expect(retrieveRecallTranscriptMock).toHaveBeenCalledWith({ - transcriptId: 'recall-transcript-1', - }); - expect(client.mutations).toEqual([ - { - id: 'call-recording-1', - data: { - status: 'PROCESSING', - externalBotId: 'recall-bot-1', - externalRecordingId: 'recall-recording-1', - }, - }, - ]); - }); - - it('resolves the recording id from the bot when the payload and record lack one', async () => { - getRecallBotMock.mockResolvedValue({ - ok: true, - bot: { - recordings: [{ id: 'recall-recording-9' }], - }, - }); - createAsyncRecallTranscriptMock.mockResolvedValue({ - ok: true, - transcriptId: 'recall-transcript-9', - }); - const client = new FakeCoreApiClient([ - { - id: 'call-recording-1', - status: 'PROCESSING', - externalBotId: 'recall-bot-1', - transcript: null, - }, - ]); - - await handleRecallWebhook({ - client: client as unknown as CoreApiClient, - body: { - event: 'bot.status_change', - data: { - bot: { - id: 'recall-bot-1', - metadata: { - twentyCallRecordingId: 'call-recording-1', - }, - }, - status: { - code: 'done', - }, - }, - }, - }); - - expect(getRecallBotMock).toHaveBeenCalledWith({ - externalBotId: 'recall-bot-1', - }); - expect(createAsyncRecallTranscriptMock).toHaveBeenCalledWith({ - externalRecordingId: 'recall-recording-9', - callRecordingId: 'call-recording-1', - }); - expect(client.mutations).toEqual([ - expect.objectContaining({ - id: 'call-recording-1', - data: expect.objectContaining({ - status: 'PROCESSING', - externalBotId: 'recall-bot-1', - externalRecordingId: 'recall-recording-9', - }), - }), - ]); - }); - - it('ingests media on recording.done and completes once all artifacts are present', async () => { - getRecallBotMock.mockResolvedValue({ - ok: true, - bot: { id: 'recall-bot-1' }, - }); - ingestCallRecordingMediaMock.mockResolvedValue({ - audio: [{ fileId: 'file-audio-1', label: 'audio.mp3' }], - video: [{ fileId: 'file-video-1', label: 'video.mp4' }], - }); - const client = new FakeCoreApiClient([ - { - id: 'call-recording-1', - status: 'PROCESSING', - externalBotId: 'recall-bot-1', - externalRecordingId: 'recall-recording-1', - startedAt: '2026-01-01T13:02:00.000Z', - endedAt: '2026-01-01T14:05:00.000Z', - transcript: [{ participant: { id: 1 }, words: [] }], - }, - ]); - - await handleRecallWebhook({ - client: client as unknown as CoreApiClient, - body: buildRecordingDoneWebhookBody(), - }); - - expect(ingestCallRecordingMediaMock).toHaveBeenCalledWith({ - callRecordingId: 'call-recording-1', - externalRecordingId: 'recall-recording-1', - hasAudio: false, - hasVideo: false, - }); - expect(client.mutations).toEqual([ - { - id: 'call-recording-1', - data: { - externalBotId: 'recall-bot-1', - externalRecordingId: 'recall-recording-1', - audio: [{ fileId: 'file-audio-1', label: 'audio.mp3' }], - video: [{ fileId: 'file-video-1', label: 'video.mp4' }], - }, - }, - { - id: 'call-recording-1', - data: { status: 'COMPLETED' }, - }, - ]); - expect(chargeCompletedCallRecordingMock).toHaveBeenCalledWith({ - callRecordingId: 'call-recording-1', - startedAt: '2026-01-01T13:02:00.000Z', - endedAt: '2026-01-01T14:05:00.000Z', - }); - }); - - it('stays PROCESSING on recording.done while artifacts are missing', async () => { - getRecallBotMock.mockResolvedValue({ - ok: true, - bot: { id: 'recall-bot-1' }, - }); - ingestCallRecordingMediaMock.mockResolvedValue({ - audio: [{ fileId: 'file-audio-1', label: 'audio.mp3' }], - }); - createAsyncRecallTranscriptMock.mockResolvedValue({ - ok: true, - transcriptId: 'recall-transcript-1', - }); - const client = new FakeCoreApiClient([ - { - id: 'call-recording-1', - status: 'PROCESSING', - externalBotId: 'recall-bot-1', - startedAt: '2026-01-01T13:02:00.000Z', - endedAt: '2026-01-01T14:05:00.000Z', - transcript: null, - }, - ]); - - await handleRecallWebhook({ - client: client as unknown as CoreApiClient, - body: buildRecordingDoneWebhookBody(), - }); - - expect(createAsyncRecallTranscriptMock).toHaveBeenCalledWith({ - externalRecordingId: 'recall-recording-1', - callRecordingId: 'call-recording-1', - }); - expect(client.mutations).toEqual([ - expect.objectContaining({ - id: 'call-recording-1', - data: expect.objectContaining({ - status: 'PROCESSING', - externalBotId: 'recall-bot-1', - externalRecordingId: 'recall-recording-1', - audio: [{ fileId: 'file-audio-1', label: 'audio.mp3' }], - }), - }), - ]); - expect(chargeCompletedCallRecordingMock).not.toHaveBeenCalled(); - }); - - it('marks FAILED on recording.done when no recording artifact path exists', async () => { - getRecallBotMock.mockResolvedValue({ - ok: true, - bot: { id: 'recall-bot-1', recordings: [] }, - }); - const client = new FakeCoreApiClient([ - { - id: 'call-recording-1', - status: 'PROCESSING', - externalBotId: 'recall-bot-1', - startedAt: '2026-01-01T13:02:00.000Z', - endedAt: '2026-01-01T14:05:00.000Z', - transcript: null, - }, - ]); - - const result = await handleRecallWebhook({ - client: client as unknown as CoreApiClient, - body: { - event: 'recording.done', - data: { - bot: { - id: 'recall-bot-1', - metadata: { - twentyWorkspaceId: WORKSPACE_ID, - }, - }, - }, - }, - }); - - expect(result).toEqual({ - status: 'updated', - event: 'recording.done', - callRecordingId: 'call-recording-1', - callRecordingStatus: 'FAILED', - }); - expect(client.mutations).toEqual([ - { - id: 'call-recording-1', - data: { - status: 'FAILED', - externalBotId: 'recall-bot-1', - meetingBotFailureReason: 'recording_artifacts_unavailable', - }, - }, - ]); - expect(chargeCompletedCallRecordingMock).not.toHaveBeenCalled(); - }); - - it('completes and charges on transcript.done when media is already ingested', async () => { - const transcriptContent = [ - { - participant: { id: 1, name: 'Alice' }, - words: [{ text: 'hello', start_timestamp: { relative: 0.5 } }], - }, - ]; - - retrieveRecallTranscriptMock.mockResolvedValue({ - ok: true, - transcript: { - downloadUrl: 'https://recall-transcripts.example.com/transcript-1', - statusCode: 'done', - statusSubCode: null, - }, - }); - vi.stubGlobal( - 'fetch', - vi.fn().mockResolvedValue({ - ok: true, - json: async () => transcriptContent, - }), - ); - - const client = new FakeCoreApiClient([ - { - id: 'call-recording-1', - status: 'PROCESSING', - externalBotId: 'recall-bot-1', - externalRecordingId: 'recall-recording-1', - startedAt: '2026-01-01T13:02:00.000Z', - endedAt: '2026-01-01T14:05:00.000Z', - transcript: { - recallTranscriptId: 'recall-transcript-1', - status: 'PENDING', - requestedAt: '2026-01-01T14:06:00.000Z', - }, - audio: [{ fileId: 'file-audio-1', label: 'audio.mp3' }], - video: [{ fileId: 'file-video-1', label: 'video.mp4' }], - }, - ]); - - const result = await handleRecallWebhook({ - client: client as unknown as CoreApiClient, - body: { - event: 'transcript.done', - data: { - bot: { - id: 'recall-bot-1', - metadata: { - twentyCallRecordingId: 'call-recording-1', - }, - }, - transcript: { - id: 'recall-transcript-1', - }, - }, - }, - }); - - expect(result).toEqual({ - status: 'updated', - event: 'transcript.done', - callRecordingId: 'call-recording-1', - transcriptOutcome: 'FILLED', - }); - expect(client.mutations).toEqual([ - { - id: 'call-recording-1', - data: { transcript: transcriptContent }, - }, - { - id: 'call-recording-1', - data: { status: 'COMPLETED' }, - }, - ]); - expect(chargeCompletedCallRecordingMock).toHaveBeenCalledWith({ - callRecordingId: 'call-recording-1', - startedAt: '2026-01-01T13:02:00.000Z', - endedAt: '2026-01-01T14:05:00.000Z', - }); - - vi.unstubAllGlobals(); - }); - - it('fills the transcript from the download URL on transcript.done', async () => { - const transcriptContent = [ - { - participant: { id: 1, name: 'Alice' }, - words: [{ text: 'hello', start_timestamp: { relative: 0.5 } }], - }, - ]; - - retrieveRecallTranscriptMock.mockResolvedValue({ - ok: true, - transcript: { - downloadUrl: 'https://recall-transcripts.example.com/transcript-1', - statusCode: 'done', - statusSubCode: null, - }, - }); - vi.stubGlobal( - 'fetch', - vi.fn().mockResolvedValue({ - ok: true, - json: async () => transcriptContent, - }), - ); - - const client = new FakeCoreApiClient([ - { - id: 'call-recording-1', - status: 'COMPLETED', - externalBotId: 'recall-bot-1', - transcript: { - recallTranscriptId: 'recall-transcript-1', - status: 'PENDING', - requestedAt: '2026-01-01T14:06:00.000Z', - }, - }, - ]); - - const result = await handleRecallWebhook({ - client: client as unknown as CoreApiClient, - body: { - event: 'transcript.done', - data: { - bot: { - id: 'recall-bot-1', - metadata: { - twentyCallRecordingId: 'call-recording-1', - }, - }, - transcript: { - id: 'recall-transcript-1', - }, - recording: { - id: 'recall-recording-1', - }, - }, - }, - }); - - expect(result).toEqual({ - status: 'updated', - event: 'transcript.done', - callRecordingId: 'call-recording-1', - transcriptOutcome: 'FILLED', - }); - expect(retrieveRecallTranscriptMock).toHaveBeenCalledWith({ - transcriptId: 'recall-transcript-1', - }); - expect(client.mutations).toEqual([ - { - id: 'call-recording-1', - data: { - transcript: transcriptContent, - externalRecordingId: 'recall-recording-1', - }, - }, - ]); - expect(chargeCompletedCallRecordingMock).not.toHaveBeenCalled(); - - vi.unstubAllGlobals(); - }); - - it('writes a FAILED marker on transcript.failed', async () => { - const client = new FakeCoreApiClient([ - { - id: 'call-recording-1', - status: 'PROCESSING', - externalBotId: 'recall-bot-1', - externalRecordingId: 'recall-recording-1', - transcript: { - recallTranscriptId: 'recall-transcript-1', - status: 'PENDING', - requestedAt: '2026-01-01T14:06:00.000Z', - }, - }, - ]); - - const result = await handleRecallWebhook({ - client: client as unknown as CoreApiClient, - body: { - event: 'transcript.failed', - data: { - bot: { - id: 'recall-bot-1', - metadata: { - twentyCallRecordingId: 'call-recording-1', - }, - }, - transcript: { - id: 'recall-transcript-1', - }, - status: { - sub_code: 'transcription_failed', - }, - }, - }, - }); - - expect(result).toEqual({ - status: 'updated', - event: 'transcript.failed', - callRecordingId: 'call-recording-1', - transcriptOutcome: 'FAILED', - }); - expect(client.mutations).toEqual([ - { - id: 'call-recording-1', - data: { - transcript: { - recallTranscriptId: 'recall-transcript-1', - status: 'FAILED', - subCode: 'transcription_failed', - }, - meetingBotFailureReason: 'transcript_failed:transcription_failed', - status: 'FAILED', - }, - }, - ]); - expect(console.warn).toHaveBeenCalled(); - }); - - it('does not clobber a downloaded transcript with a late transcript.failed', async () => { - const client = new FakeCoreApiClient([ - { - id: 'call-recording-1', - status: 'COMPLETED', - externalBotId: 'recall-bot-1', - transcript: [{ participant: { id: 1 }, words: [] }], - }, - ]); - - const result = await handleRecallWebhook({ - client: client as unknown as CoreApiClient, - body: { - event: 'transcript.failed', - data: { - bot: { - id: 'recall-bot-1', - metadata: { - twentyCallRecordingId: 'call-recording-1', - }, - }, - transcript: { - id: 'recall-transcript-1', - }, - status: { - sub_code: 'transcription_failed', - }, - }, - }, - }); - - expect(result).toEqual({ - status: 'skipped', - event: 'transcript.failed', - reason: 'transcript already filled', - }); - expect(client.mutations).toEqual([]); - }); -}); diff --git a/packages/twenty-apps/public/twenty-meeting-bot/src/logic-functions/flows/__tests__/heal-call-recordings-missing-bot.test.ts b/packages/twenty-apps/public/twenty-meeting-bot/src/logic-functions/flows/__tests__/heal-call-recordings-missing-bot.test.ts deleted file mode 100644 index 4d732bd9c1c7b..0000000000000 --- a/packages/twenty-apps/public/twenty-meeting-bot/src/logic-functions/flows/__tests__/heal-call-recordings-missing-bot.test.ts +++ /dev/null @@ -1,225 +0,0 @@ -import { type CoreApiClient } from 'twenty-client-sdk/core'; -import { beforeEach, describe, expect, it, vi } from 'vitest'; - -import { healCallRecordingsMissingBot } from 'src/logic-functions/flows/heal-call-recordings-missing-bot.util'; - -const scheduleRecallBotMock = vi.hoisted(() => vi.fn()); -const getCurrentWorkspaceIdMock = vi.hoisted(() => vi.fn()); - -vi.mock('src/logic-functions/data/get-current-workspace-id.util', () => ({ - getCurrentWorkspaceId: getCurrentWorkspaceIdMock, -})); - -vi.mock('src/logic-functions/recall-api/schedule-recall-bot.util', () => ({ - scheduleRecallBot: scheduleRecallBotMock, -})); - -const NOW = new Date('2026-01-01T12:00:00.000Z'); -const WORKSPACE_ID = '123e4567-e89b-12d3-a456-426614174000'; -const UPCOMING_STARTS_AT = '2026-01-01T13:00:00.000Z'; -const UPCOMING_ENDS_AT = '2026-01-01T14:00:00.000Z'; -const PAST_STARTS_AT = '2026-01-01T10:00:00.000Z'; -const PAST_ENDS_AT = '2026-01-01T11:00:00.000Z'; - -type CallRecordingNode = { - id: string; - status?: string; - recordingRequestStatus?: string | null; - calendarEventId?: string | null; - externalBotId?: string | null; -}; - -type CalendarEventNode = { - id: string; - startsAt?: string | null; - endsAt?: string | null; - iCalUid?: string | null; - conferenceLink?: { primaryLinkUrl?: string | null } | null; -}; - -class FakeCoreApiClient { - callRecordings: CallRecordingNode[]; - calendarEvents: CalendarEventNode[]; - - constructor({ - callRecordings = [], - calendarEvents = [], - }: { - callRecordings?: CallRecordingNode[]; - calendarEvents?: CalendarEventNode[]; - }) { - this.callRecordings = callRecordings; - this.calendarEvents = calendarEvents; - } - - async query(query: any): Promise { - if (query.callRecordings !== undefined) { - const filter = query.callRecordings.__args.filter; - const matches = - filter.id?.in !== undefined - ? this.callRecordings.filter((callRecording) => - filter.id.in.includes(callRecording.id), - ) - : this.callRecordings.filter( - (callRecording) => - callRecording.recordingRequestStatus === - filter.recordingRequestStatus.eq && - callRecording.status === filter.status.eq, - ); - - return { callRecordings: buildConnection(matches) }; - } - - if (query.calendarEvents !== undefined) { - const calendarEventIds = query.calendarEvents.__args.filter.id.in; - - return { - calendarEvents: buildConnection( - this.calendarEvents.filter((calendarEvent) => - calendarEventIds.includes(calendarEvent.id), - ), - ), - }; - } - - throw new Error(`Unhandled query: ${JSON.stringify(query)}`); - } - - async mutation(mutation: any): Promise { - if (mutation.updateCallRecording !== undefined) { - const { id, data } = mutation.updateCallRecording.__args; - const callRecording = this.callRecordings.find( - (candidate) => candidate.id === id, - ); - - if (callRecording !== undefined) { - Object.assign(callRecording, data); - } - - return { updateCallRecording: { id } }; - } - - throw new Error(`Unhandled mutation: ${JSON.stringify(mutation)}`); - } -} - -const buildConnection = (nodes: Node[]) => ({ - pageInfo: { hasNextPage: false, endCursor: undefined }, - edges: nodes.map((node) => ({ node })), -}); - -const buildBotlessCallRecording = ( - overrides: Partial = {}, -): CallRecordingNode => ({ - id: 'call-recording-1', - status: 'SCHEDULED', - recordingRequestStatus: 'REQUESTED', - calendarEventId: 'calendar-event-1', - externalBotId: null, - ...overrides, -}); - -const buildCalendarEvent = ( - overrides: Partial = {}, -): CalendarEventNode => ({ - id: 'calendar-event-1', - startsAt: UPCOMING_STARTS_AT, - endsAt: UPCOMING_ENDS_AT, - iCalUid: 'calendar-event-uid', - conferenceLink: { primaryLinkUrl: 'https://meet.example.com/customer-sync' }, - ...overrides, -}); - -describe('healCallRecordingsMissingBot', () => { - beforeEach(() => { - vi.spyOn(console, 'warn').mockImplementation(() => {}); - getCurrentWorkspaceIdMock.mockReset(); - getCurrentWorkspaceIdMock.mockReturnValue(WORKSPACE_ID); - scheduleRecallBotMock.mockReset(); - scheduleRecallBotMock.mockResolvedValue({ - ok: true, - externalBotId: 'recall-bot-1', - }); - }); - - it('schedules a bot and writes the id for an upcoming botless recording', async () => { - const client = new FakeCoreApiClient({ - callRecordings: [buildBotlessCallRecording()], - calendarEvents: [buildCalendarEvent()], - }); - - const result = await healCallRecordingsMissingBot({ - client: client as unknown as CoreApiClient, - now: NOW, - }); - - expect(result.scheduledCallRecordingIds).toEqual(['call-recording-1']); - expect(scheduleRecallBotMock).toHaveBeenCalledTimes(1); - expect(scheduleRecallBotMock).toHaveBeenCalledWith( - expect.objectContaining({ - metadata: expect.objectContaining({ - twentyWorkspaceId: WORKSPACE_ID, - }), - }), - ); - expect(client.callRecordings[0].externalBotId).toBe('recall-bot-1'); - }); - - it('does not report a recording as scheduled when Recall scheduling fails', async () => { - scheduleRecallBotMock.mockResolvedValue({ - ok: false, - status: 500, - errorMessage: 'Recall API responded with HTTP 500', - }); - const client = new FakeCoreApiClient({ - callRecordings: [buildBotlessCallRecording()], - calendarEvents: [buildCalendarEvent()], - }); - - const result = await healCallRecordingsMissingBot({ - client: client as unknown as CoreApiClient, - now: NOW, - }); - - expect(result.scheduledCallRecordingIds).toEqual([]); - expect(scheduleRecallBotMock).toHaveBeenCalledTimes(1); - expect(client.callRecordings[0].externalBotId).toBeNull(); - }); - - it('skips a recording whose meeting has already ended', async () => { - const client = new FakeCoreApiClient({ - callRecordings: [buildBotlessCallRecording()], - calendarEvents: [ - buildCalendarEvent({ - startsAt: PAST_STARTS_AT, - endsAt: PAST_ENDS_AT, - }), - ], - }); - - const result = await healCallRecordingsMissingBot({ - client: client as unknown as CoreApiClient, - now: NOW, - }); - - expect(result.scheduledCallRecordingIds).toEqual([]); - expect(scheduleRecallBotMock).not.toHaveBeenCalled(); - }); - - it('does nothing when every scheduled recording already has a bot', async () => { - const client = new FakeCoreApiClient({ - callRecordings: [ - buildBotlessCallRecording({ externalBotId: 'recall-bot-existing' }), - ], - calendarEvents: [buildCalendarEvent()], - }); - - const result = await healCallRecordingsMissingBot({ - client: client as unknown as CoreApiClient, - now: NOW, - }); - - expect(result.scheduledCallRecordingIds).toEqual([]); - expect(scheduleRecallBotMock).not.toHaveBeenCalled(); - }); -}); diff --git a/packages/twenty-apps/public/twenty-meeting-bot/src/logic-functions/flows/__tests__/ingest-call-recording-media.test.ts b/packages/twenty-apps/public/twenty-meeting-bot/src/logic-functions/flows/__tests__/ingest-call-recording-media.test.ts deleted file mode 100644 index 3506fd250552a..0000000000000 --- a/packages/twenty-apps/public/twenty-meeting-bot/src/logic-functions/flows/__tests__/ingest-call-recording-media.test.ts +++ /dev/null @@ -1,153 +0,0 @@ -import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; - -import { ingestCallRecordingMedia } from 'src/logic-functions/flows/ingest-call-recording-media.util'; - -const uploadFileMock = vi.hoisted(() => vi.fn()); -const getRecallRecordingMock = vi.hoisted(() => vi.fn()); - -vi.mock('twenty-client-sdk/metadata', () => ({ - MetadataApiClient: class { - uploadFile = uploadFileMock; - }, -})); - -vi.mock('src/logic-functions/recall-api/get-recall-recording.util', () => ({ - getRecallRecording: getRecallRecordingMock, -})); - -const RECORDING_WITH_MEDIA = { - id: 'recall-recording-1', - media_shortcuts: { - video_mixed: { download_url: 'https://media.example.com/video.mp4' }, - audio_mixed: { download_url: 'https://media.example.com/audio.mp3' }, - }, -}; - -const buildFetchResponse = () => ({ - ok: true, - headers: { get: () => 'video/mp4' }, - arrayBuffer: async () => new ArrayBuffer(8), -}); - -describe('ingestCallRecordingMedia', () => { - beforeEach(() => { - vi.spyOn(console, 'warn').mockImplementation(() => {}); - uploadFileMock.mockReset(); - getRecallRecordingMock.mockReset(); - getRecallRecordingMock.mockResolvedValue({ - ok: true, - recording: RECORDING_WITH_MEDIA, - }); - vi.stubGlobal('fetch', vi.fn().mockResolvedValue(buildFetchResponse())); - }); - - afterEach(() => { - vi.unstubAllGlobals(); - }); - - it('downloads and uploads every missing artifact', async () => { - uploadFileMock - .mockResolvedValueOnce({ id: 'file-video-1' }) - .mockResolvedValueOnce({ id: 'file-audio-1' }); - - const updateFields = await ingestCallRecordingMedia({ - callRecordingId: 'call-recording-1', - externalRecordingId: 'recall-recording-1', - hasAudio: false, - hasVideo: false, - }); - - expect(updateFields).toEqual({ - video: [{ fileId: 'file-video-1', label: 'video.mp4' }], - audio: [{ fileId: 'file-audio-1', label: 'audio.mp3' }], - }); - expect(uploadFileMock).toHaveBeenCalledTimes(2); - }); - - it('skips artifacts already on the record', async () => { - uploadFileMock.mockResolvedValue({ id: 'file-audio-1' }); - - const updateFields = await ingestCallRecordingMedia({ - callRecordingId: 'call-recording-1', - externalRecordingId: 'recall-recording-1', - hasAudio: false, - hasVideo: true, - }); - - expect(updateFields).toEqual({ - audio: [{ fileId: 'file-audio-1', label: 'audio.mp3' }], - }); - expect(uploadFileMock).toHaveBeenCalledTimes(1); - }); - - it('does not fetch the recording when both artifacts are present', async () => { - const updateFields = await ingestCallRecordingMedia({ - callRecordingId: 'call-recording-1', - externalRecordingId: 'recall-recording-1', - hasAudio: true, - hasVideo: true, - }); - - expect(updateFields).toEqual({}); - expect(getRecallRecordingMock).not.toHaveBeenCalled(); - expect(uploadFileMock).not.toHaveBeenCalled(); - }); - - it('omits an artifact and warns when its transfer fails', async () => { - uploadFileMock.mockRejectedValueOnce(new Error('upload exploded')); - uploadFileMock.mockResolvedValueOnce({ id: 'file-audio-1' }); - - const updateFields = await ingestCallRecordingMedia({ - callRecordingId: 'call-recording-1', - externalRecordingId: 'recall-recording-1', - hasAudio: false, - hasVideo: false, - }); - - expect(updateFields).toEqual({ - audio: [{ fileId: 'file-audio-1', label: 'audio.mp3' }], - }); - expect(Object.keys(updateFields)).toEqual(['audio']); - expect(console.warn).toHaveBeenCalledWith( - expect.stringContaining('upload exploded'), - ); - }); - - it('returns nothing when the recording exposes no media urls', async () => { - getRecallRecordingMock.mockResolvedValue({ - ok: true, - recording: { id: 'recall-recording-1' }, - }); - - const updateFields = await ingestCallRecordingMedia({ - callRecordingId: 'call-recording-1', - externalRecordingId: 'recall-recording-1', - hasAudio: false, - hasVideo: false, - }); - - expect(updateFields).toEqual({}); - expect(uploadFileMock).not.toHaveBeenCalled(); - }); - - it('warns and returns nothing when the recording fetch fails', async () => { - getRecallRecordingMock.mockResolvedValue({ - ok: false, - status: 500, - errorMessage: 'recording boom', - }); - - const updateFields = await ingestCallRecordingMedia({ - callRecordingId: 'call-recording-1', - externalRecordingId: 'recall-recording-1', - hasAudio: false, - hasVideo: false, - }); - - expect(updateFields).toEqual({}); - expect(uploadFileMock).not.toHaveBeenCalled(); - expect(console.warn).toHaveBeenCalledWith( - expect.stringContaining('recording boom'), - ); - }); -}); diff --git a/packages/twenty-apps/public/twenty-meeting-bot/src/logic-functions/flows/__tests__/reap-orphaned-meeting-bots.test.ts b/packages/twenty-apps/public/twenty-meeting-bot/src/logic-functions/flows/__tests__/reap-orphaned-meeting-bots.test.ts deleted file mode 100644 index e99d6d66a5092..0000000000000 --- a/packages/twenty-apps/public/twenty-meeting-bot/src/logic-functions/flows/__tests__/reap-orphaned-meeting-bots.test.ts +++ /dev/null @@ -1,425 +0,0 @@ -import { type CoreApiClient } from 'twenty-client-sdk/core'; -import { beforeEach, describe, expect, it, vi } from 'vitest'; - -import { reapOrphanedMeetingBots } from 'src/logic-functions/flows/reap-orphaned-meeting-bots.util'; - -const listScheduledRecallBotsMock = vi.hoisted(() => vi.fn()); -const cancelRecallBotMock = vi.hoisted(() => vi.fn()); -const ejectRecallBotMock = vi.hoisted(() => vi.fn()); -const getCurrentWorkspaceIdMock = vi.hoisted(() => vi.fn()); - -vi.mock('src/logic-functions/data/get-current-workspace-id.util', () => ({ - getCurrentWorkspaceId: getCurrentWorkspaceIdMock, -})); - -vi.mock( - 'src/logic-functions/recall-api/list-scheduled-recall-bots.util', - () => ({ - listScheduledRecallBots: listScheduledRecallBotsMock, - }), -); - -vi.mock('src/logic-functions/recall-api/cancel-recall-bot.util', () => ({ - cancelRecallBot: cancelRecallBotMock, -})); - -vi.mock('src/logic-functions/recall-api/eject-recall-bot.util', () => ({ - ejectRecallBot: ejectRecallBotMock, -})); - -const JOIN_AT_AFTER = '2026-01-01T08:00:00.000Z'; -const JOIN_AT_BEFORE = '2026-01-02T12:00:00.000Z'; -const CURRENT_WORKSPACE_ID = '123e4567-e89b-12d3-a456-426614174000'; -const OTHER_WORKSPACE_ID = '123e4567-e89b-12d3-a456-426614174999'; - -type CallRecordingNode = { - id: string; - recordingRequestStatus?: string | null; - externalBotId?: string | null; -}; - -class FakeCoreApiClient { - constructor(private callRecordings: CallRecordingNode[]) {} - - async query(query: any): Promise { - const callRecordingIds = query.callRecordings.__args.filter.id.in; - - return { - callRecordings: { - pageInfo: { hasNextPage: false, endCursor: undefined }, - edges: this.callRecordings - .filter((callRecording) => - callRecordingIds.includes(callRecording.id), - ) - .map((node) => ({ node })), - }, - }; - } -} - -const buildClient = (callRecordings: CallRecordingNode[]): CoreApiClient => - new FakeCoreApiClient(callRecordings) as unknown as CoreApiClient; - -const buildBot = ({ - id, - twentyCallRecordingId, - twentyWorkspaceId, -}: { - id: string; - twentyCallRecordingId?: string; - twentyWorkspaceId?: string; -}) => ({ - id, - metadata: { - ...(twentyCallRecordingId === undefined ? {} : { twentyCallRecordingId }), - ...(twentyWorkspaceId === undefined ? {} : { twentyWorkspaceId }), - }, -}); - -const buildCurrentWorkspaceBot = ({ - id, - twentyCallRecordingId, -}: { - id: string; - twentyCallRecordingId: string; -}) => - buildBot({ - id, - twentyCallRecordingId, - twentyWorkspaceId: CURRENT_WORKSPACE_ID, - }); - -describe('reapOrphanedMeetingBots', () => { - beforeEach(() => { - vi.spyOn(console, 'warn').mockImplementation(() => {}); - getCurrentWorkspaceIdMock.mockReset(); - getCurrentWorkspaceIdMock.mockReturnValue(CURRENT_WORKSPACE_ID); - listScheduledRecallBotsMock.mockReset(); - cancelRecallBotMock.mockReset(); - cancelRecallBotMock.mockResolvedValue({ ok: true }); - ejectRecallBotMock.mockReset(); - ejectRecallBotMock.mockResolvedValue({ ok: true }); - }); - - it('keeps bots that their call recording still references', async () => { - listScheduledRecallBotsMock.mockResolvedValue({ - ok: true, - bots: [ - buildCurrentWorkspaceBot({ - id: 'claimed-bot', - twentyCallRecordingId: 'call-recording-1', - }), - ], - }); - - const result = await reapOrphanedMeetingBots({ - client: buildClient([ - { - id: 'call-recording-1', - recordingRequestStatus: 'REQUESTED', - externalBotId: 'claimed-bot', - }, - ]), - joinAtAfter: JOIN_AT_AFTER, - joinAtBefore: JOIN_AT_BEFORE, - }); - - expect(result).toEqual({ - scannedBotCount: 1, - canceledExternalBotIds: [], - }); - expect(cancelRecallBotMock).not.toHaveBeenCalled(); - }); - - it('cancels bots whose call recording request was canceled locally', async () => { - listScheduledRecallBotsMock.mockResolvedValue({ - ok: true, - bots: [ - buildCurrentWorkspaceBot({ - id: 'stale-cancel-bot', - twentyCallRecordingId: 'call-recording-1', - }), - ], - }); - - const result = await reapOrphanedMeetingBots({ - client: buildClient([ - { - id: 'call-recording-1', - recordingRequestStatus: 'CANCELED', - externalBotId: 'stale-cancel-bot', - }, - ]), - joinAtAfter: JOIN_AT_AFTER, - joinAtBefore: JOIN_AT_BEFORE, - }); - - expect(result).toEqual({ - scannedBotCount: 1, - canceledExternalBotIds: ['stale-cancel-bot'], - }); - expect(cancelRecallBotMock).toHaveBeenCalledWith({ - externalBotId: 'stale-cancel-bot', - }); - }); - - it('cancels bots whose call recording references another bot', async () => { - listScheduledRecallBotsMock.mockResolvedValue({ - ok: true, - bots: [ - buildCurrentWorkspaceBot({ - id: 'superseded-bot', - twentyCallRecordingId: 'call-recording-1', - }), - buildCurrentWorkspaceBot({ - id: 'claimed-bot', - twentyCallRecordingId: 'call-recording-1', - }), - ], - }); - - const result = await reapOrphanedMeetingBots({ - client: buildClient([ - { - id: 'call-recording-1', - recordingRequestStatus: 'REQUESTED', - externalBotId: 'claimed-bot', - }, - ]), - joinAtAfter: JOIN_AT_AFTER, - joinAtBefore: JOIN_AT_BEFORE, - }); - - expect(result).toEqual({ - scannedBotCount: 2, - canceledExternalBotIds: ['superseded-bot'], - }); - expect(cancelRecallBotMock).toHaveBeenCalledTimes(1); - expect(cancelRecallBotMock).toHaveBeenCalledWith({ - externalBotId: 'superseded-bot', - }); - }); - - it('cancels bots whose call recording no longer exists', async () => { - listScheduledRecallBotsMock.mockResolvedValue({ - ok: true, - bots: [ - buildCurrentWorkspaceBot({ - id: 'orphan-bot', - twentyCallRecordingId: 'call-recording-gone', - }), - ], - }); - - const result = await reapOrphanedMeetingBots({ - client: buildClient([]), - joinAtAfter: JOIN_AT_AFTER, - joinAtBefore: JOIN_AT_BEFORE, - }); - - expect(result).toEqual({ - scannedBotCount: 1, - canceledExternalBotIds: ['orphan-bot'], - }); - }); - - it('grants a grace round to requested recordings without a bot id yet', async () => { - listScheduledRecallBotsMock.mockResolvedValue({ - ok: true, - bots: [ - buildCurrentWorkspaceBot({ - id: 'pending-bot', - twentyCallRecordingId: 'call-recording-1', - }), - ], - }); - - const result = await reapOrphanedMeetingBots({ - client: buildClient([ - { - id: 'call-recording-1', - recordingRequestStatus: 'REQUESTED', - externalBotId: null, - }, - ]), - joinAtAfter: JOIN_AT_AFTER, - joinAtBefore: JOIN_AT_BEFORE, - }); - - expect(result).toEqual({ - scannedBotCount: 1, - canceledExternalBotIds: [], - }); - expect(cancelRecallBotMock).not.toHaveBeenCalled(); - }); - - it('ignores bots that were not created by this app', async () => { - listScheduledRecallBotsMock.mockResolvedValue({ - ok: true, - bots: [buildBot({ id: 'unrelated-bot' })], - }); - - const result = await reapOrphanedMeetingBots({ - client: buildClient([]), - joinAtAfter: JOIN_AT_AFTER, - joinAtBefore: JOIN_AT_BEFORE, - }); - - expect(result).toEqual({ - scannedBotCount: 1, - canceledExternalBotIds: [], - }); - expect(cancelRecallBotMock).not.toHaveBeenCalled(); - }); - - it('ignores untagged bots even when they carry call recording metadata', async () => { - listScheduledRecallBotsMock.mockResolvedValue({ - ok: true, - bots: [ - buildBot({ - id: 'untagged-bot', - twentyCallRecordingId: 'call-recording-gone', - }), - ], - }); - - const result = await reapOrphanedMeetingBots({ - client: buildClient([]), - joinAtAfter: JOIN_AT_AFTER, - joinAtBefore: JOIN_AT_BEFORE, - }); - - expect(result).toEqual({ - scannedBotCount: 1, - canceledExternalBotIds: [], - }); - expect(cancelRecallBotMock).not.toHaveBeenCalled(); - }); - - it('ignores bots claimed by another workspace', async () => { - listScheduledRecallBotsMock.mockResolvedValue({ - ok: true, - bots: [ - buildBot({ - id: 'other-workspace-bot', - twentyCallRecordingId: 'call-recording-gone', - twentyWorkspaceId: OTHER_WORKSPACE_ID, - }), - ], - }); - - const result = await reapOrphanedMeetingBots({ - client: buildClient([]), - joinAtAfter: JOIN_AT_AFTER, - joinAtBefore: JOIN_AT_BEFORE, - }); - - expect(result).toEqual({ - scannedBotCount: 1, - canceledExternalBotIds: [], - }); - expect(cancelRecallBotMock).not.toHaveBeenCalled(); - }); - - it('cancels orphaned bots claimed by this workspace', async () => { - listScheduledRecallBotsMock.mockResolvedValue({ - ok: true, - bots: [ - buildCurrentWorkspaceBot({ - id: 'same-workspace-bot', - twentyCallRecordingId: 'call-recording-gone', - }), - ], - }); - - const result = await reapOrphanedMeetingBots({ - client: buildClient([]), - joinAtAfter: JOIN_AT_AFTER, - joinAtBefore: JOIN_AT_BEFORE, - }); - - expect(result).toEqual({ - scannedBotCount: 1, - canceledExternalBotIds: ['same-workspace-bot'], - }); - expect(cancelRecallBotMock).toHaveBeenCalledWith({ - externalBotId: 'same-workspace-bot', - }); - }); - - it('ejects an orphaned bot that already joined when deletion is rejected', async () => { - listScheduledRecallBotsMock.mockResolvedValue({ - ok: true, - bots: [ - buildCurrentWorkspaceBot({ - id: 'in-call-orphan', - twentyCallRecordingId: 'call-recording-gone', - }), - ], - }); - cancelRecallBotMock.mockResolvedValue({ - ok: false, - status: 409, - errorMessage: 'Recall API responded with HTTP 409', - }); - - const result = await reapOrphanedMeetingBots({ - client: buildClient([]), - joinAtAfter: JOIN_AT_AFTER, - joinAtBefore: JOIN_AT_BEFORE, - }); - - expect(result).toEqual({ - scannedBotCount: 1, - canceledExternalBotIds: ['in-call-orphan'], - }); - expect(ejectRecallBotMock).toHaveBeenCalledWith({ - externalBotId: 'in-call-orphan', - }); - }); - - it('reports nothing reaped when listing Recall bots fails', async () => { - listScheduledRecallBotsMock.mockResolvedValue({ - ok: false, - status: 500, - errorMessage: 'Recall API responded with HTTP 500', - }); - - const result = await reapOrphanedMeetingBots({ - client: buildClient([]), - joinAtAfter: JOIN_AT_AFTER, - joinAtBefore: JOIN_AT_BEFORE, - }); - - expect(result).toEqual({ - scannedBotCount: 0, - canceledExternalBotIds: [], - }); - expect(cancelRecallBotMock).not.toHaveBeenCalled(); - }); - - it('skips reaping when the current workspace cannot be resolved', async () => { - getCurrentWorkspaceIdMock.mockReturnValue(undefined); - listScheduledRecallBotsMock.mockResolvedValue({ - ok: true, - bots: [ - buildCurrentWorkspaceBot({ - id: 'same-workspace-bot', - twentyCallRecordingId: 'call-recording-gone', - }), - ], - }); - - const result = await reapOrphanedMeetingBots({ - client: buildClient([]), - joinAtAfter: JOIN_AT_AFTER, - joinAtBefore: JOIN_AT_BEFORE, - }); - - expect(result).toEqual({ - scannedBotCount: 1, - canceledExternalBotIds: [], - }); - expect(cancelRecallBotMock).not.toHaveBeenCalled(); - }); -}); diff --git a/packages/twenty-apps/public/twenty-meeting-bot/src/logic-functions/flows/__tests__/reconcile-meeting-bot.test.ts b/packages/twenty-apps/public/twenty-meeting-bot/src/logic-functions/flows/__tests__/reconcile-meeting-bot.test.ts deleted file mode 100644 index 6729dcd74fa78..0000000000000 --- a/packages/twenty-apps/public/twenty-meeting-bot/src/logic-functions/flows/__tests__/reconcile-meeting-bot.test.ts +++ /dev/null @@ -1,1013 +0,0 @@ -import { type CoreApiClient } from 'twenty-client-sdk/core'; -import { beforeEach, describe, expect, it, vi } from 'vitest'; - -import { computeCallRecordingIdForMeeting } from 'src/logic-functions/domain/compute-call-recording-id-for-meeting.util'; -import { reconcileMeetingBotForCalendarEventIds } from 'src/logic-functions/flows/reconcile-meeting-bot.util'; - -const scheduleRecallBotMock = vi.hoisted(() => vi.fn()); -const rescheduleRecallBotMock = vi.hoisted(() => vi.fn()); -const cancelRecallBotMock = vi.hoisted(() => vi.fn()); -const getCurrentWorkspaceIdMock = vi.hoisted(() => vi.fn()); - -vi.mock('src/logic-functions/data/get-current-workspace-id.util', () => ({ - getCurrentWorkspaceId: getCurrentWorkspaceIdMock, -})); - -vi.mock('src/logic-functions/recall-api/schedule-recall-bot.util', () => ({ - scheduleRecallBot: scheduleRecallBotMock, -})); - -vi.mock('src/logic-functions/recall-api/reschedule-recall-bot.util', () => ({ - rescheduleRecallBot: rescheduleRecallBotMock, -})); - -vi.mock('src/logic-functions/recall-api/cancel-recall-bot.util', () => ({ - cancelRecallBot: cancelRecallBotMock, -})); - -const NOW = new Date('2026-01-01T12:00:00.000Z'); -const WORKSPACE_ID = '123e4567-e89b-12d3-a456-426614174000'; -const FUTURE_STARTS_AT = '2026-01-01T13:00:00.000Z'; -const FUTURE_RECALL_BOT_JOIN_AT = '2026-01-01T12:59:00.000Z'; -const FUTURE_ENDS_AT = '2026-01-01T14:00:00.000Z'; - -const buildCustomerSyncCallRecordingId = ( - startsAt: string = FUTURE_STARTS_AT, -): string => - computeCallRecordingIdForMeeting( - `link:meet.example.com/customer-sync:${startsAt}`, - ); - -type CalendarEventNode = { - id: string; - title?: string | null; - isCanceled?: boolean | null; - startsAt?: string | null; - endsAt?: string | null; - iCalUid?: string | null; - conferenceLink?: { primaryLinkUrl?: string | null } | null; - meetingBotPreference?: string | null; -}; - -type CallRecordingNode = { - id: string; - title?: string | null; - status?: string | null; - recordingRequestStatus?: string | null; - startedAt?: string | null; - endedAt?: string | null; - calendarEventId?: string | null; - externalBotId?: string | null; - externalRecordingId?: string | null; -}; - -type FakeCoreApiClientFixture = { - calendarEvents: CalendarEventNode[]; - callRecordings?: CallRecordingNode[]; -}; - -class FakeCoreApiClient { - calendarEvents: CalendarEventNode[]; - callRecordings: CallRecordingNode[]; - mutations: Array<{ name: string; args: unknown }> = []; - - constructor({ - calendarEvents, - callRecordings = [], - }: FakeCoreApiClientFixture) { - this.calendarEvents = calendarEvents; - this.callRecordings = callRecordings; - } - - async query(query: any): Promise { - if (query.calendarEvents !== undefined) { - return { - calendarEvents: buildConnection( - this.filterCalendarEvents(query.calendarEvents.__args.filter), - ), - }; - } - - if (query.callRecordings !== undefined) { - const filter = query.callRecordings.__args.filter; - - if (filter.id?.in !== undefined) { - return { - callRecordings: buildConnection( - this.callRecordings.filter((callRecording) => - filter.id.in.includes(callRecording.id), - ), - ), - }; - } - - return { - callRecordings: buildConnection( - this.callRecordings.filter((callRecording) => - filter.calendarEventId.in.includes(callRecording.calendarEventId), - ), - ), - }; - } - - throw new Error(`Unhandled query: ${JSON.stringify(query)}`); - } - - async mutation(mutation: any): Promise { - if (mutation.createCallRecording !== undefined) { - const data = mutation.createCallRecording.__args.data; - - if (this.callRecordings.some((candidate) => candidate.id === data.id)) { - throw new Error(`Duplicate call recording id ${data.id}`); - } - - const createdCallRecording = { ...data }; - - this.callRecordings.push(createdCallRecording); - this.mutations.push({ - name: 'createCallRecording', - args: data, - }); - - return { - createCallRecording: { - id: createdCallRecording.id, - }, - }; - } - - if (mutation.updateCallRecording !== undefined) { - const { id, data } = mutation.updateCallRecording.__args; - const callRecording = this.callRecordings.find( - (candidate) => candidate.id === id, - ); - - if (callRecording === undefined) { - throw new Error(`Could not find call recording ${id}`); - } - - Object.assign(callRecording, data); - this.mutations.push({ - name: 'updateCallRecording', - args: { id, data }, - }); - - return { - updateCallRecording: { - id, - }, - }; - } - - throw new Error(`Unhandled mutation: ${JSON.stringify(mutation)}`); - } - - private filterCalendarEvents(filter: any): CalendarEventNode[] { - if (filter.id?.in !== undefined) { - return this.calendarEvents.filter((calendarEvent) => - filter.id.in.includes(calendarEvent.id), - ); - } - - if (filter.startsAt?.in !== undefined) { - return this.calendarEvents.filter((calendarEvent) => - filter.startsAt.in.includes(calendarEvent.startsAt), - ); - } - - throw new Error( - `Unhandled calendar event filter: ${JSON.stringify(filter)}`, - ); - } -} - -const buildConnection = (nodes: Node[]) => ({ - pageInfo: { - hasNextPage: false, - endCursor: undefined, - }, - edges: nodes.map((node) => ({ node })), -}); - -const buildCalendarEvent = ( - overrides: Partial = {}, -): CalendarEventNode => ({ - id: 'calendar-event-1', - title: 'Customer Sync', - isCanceled: false, - startsAt: FUTURE_STARTS_AT, - endsAt: FUTURE_ENDS_AT, - iCalUid: 'calendar-event-uid', - conferenceLink: { - primaryLinkUrl: 'https://meet.example.com/customer-sync', - }, - meetingBotPreference: 'ON', - ...overrides, -}); - -const buildFakeCoreApiClient = ( - fixture: FakeCoreApiClientFixture, -): FakeCoreApiClient => new FakeCoreApiClient(fixture); - -describe('reconcileMeetingBotForCalendarEventIds', () => { - beforeEach(() => { - vi.spyOn(console, 'warn').mockImplementation(() => {}); - vi.spyOn(console, 'error').mockImplementation(() => {}); - getCurrentWorkspaceIdMock.mockReset(); - getCurrentWorkspaceIdMock.mockReturnValue(WORKSPACE_ID); - scheduleRecallBotMock.mockReset(); - scheduleRecallBotMock.mockResolvedValue({ - ok: true, - externalBotId: 'recall-bot-1', - }); - rescheduleRecallBotMock.mockReset(); - rescheduleRecallBotMock.mockResolvedValue({ - ok: true, - externalBotId: 'recall-bot-1', - }); - cancelRecallBotMock.mockReset(); - cancelRecallBotMock.mockResolvedValue({ - ok: true, - externalBotId: null, - }); - }); - - it('creates a scheduled call recording when the policy requests a bot', async () => { - const client = buildFakeCoreApiClient({ - calendarEvents: [buildCalendarEvent()], - }); - - const result = await reconcileMeetingBotForCalendarEventIds({ - client: client as unknown as CoreApiClient, - calendarEventIds: ['calendar-event-1'], - now: NOW, - }); - - expect(result).toEqual([ - expect.objectContaining({ - action: 'CREATED', - callRecordingId: buildCustomerSyncCallRecordingId(), - }), - ]); - expect(client.callRecordings).toEqual([ - { - id: buildCustomerSyncCallRecordingId(), - title: 'Customer Sync', - status: 'SCHEDULED', - recordingRequestStatus: 'REQUESTED', - calendarEventId: 'calendar-event-1', - externalBotId: 'recall-bot-1', - }, - ]); - expect(scheduleRecallBotMock).toHaveBeenCalledWith({ - meetingUrl: 'https://meet.example.com/customer-sync', - joinAt: FUTURE_RECALL_BOT_JOIN_AT, - metadata: { - twentyWorkspaceId: WORKSPACE_ID, - twentyCallRecordingId: buildCustomerSyncCallRecordingId(), - twentyCalendarEventId: 'calendar-event-1', - twentyRealMeetingKey: - 'link:meet.example.com/customer-sync:2026-01-01T13:00:00.000Z', - }, - }); - }); - - it('creates a scheduled call recording for the default ON preference', async () => { - const client = buildFakeCoreApiClient({ - calendarEvents: [buildCalendarEvent({ meetingBotPreference: null })], - }); - - const result = await reconcileMeetingBotForCalendarEventIds({ - client: client as unknown as CoreApiClient, - calendarEventIds: ['calendar-event-1'], - now: NOW, - }); - - expect(result).toEqual([ - expect.objectContaining({ - action: 'CREATED', - callRecordingId: buildCustomerSyncCallRecordingId(), - }), - ]); - expect(scheduleRecallBotMock).toHaveBeenCalledTimes(1); - }); - - it('creates a recording for an in-progress meeting that has not ended', async () => { - const client = buildFakeCoreApiClient({ - calendarEvents: [ - buildCalendarEvent({ - meetingBotPreference: null, - startsAt: '2026-01-01T11:30:00.000Z', - endsAt: '2026-01-01T13:00:00.000Z', - }), - ], - }); - - const result = await reconcileMeetingBotForCalendarEventIds({ - client: client as unknown as CoreApiClient, - calendarEventIds: ['calendar-event-1'], - now: NOW, - }); - - expect(result).toEqual([ - expect.objectContaining({ - action: 'CREATED', - callRecordingId: buildCustomerSyncCallRecordingId( - '2026-01-01T11:30:00.000Z', - ), - }), - ]); - expect(scheduleRecallBotMock).toHaveBeenCalledTimes(1); - }); - - it('updates an existing in-progress recording', async () => { - const client = buildFakeCoreApiClient({ - calendarEvents: [ - buildCalendarEvent({ - meetingBotPreference: null, - title: 'Updated Customer Sync', - startsAt: '2026-01-01T11:30:00.000Z', - endsAt: '2026-01-01T13:00:00.000Z', - }), - ], - callRecordings: [ - { - id: buildCustomerSyncCallRecordingId('2026-01-01T11:30:00.000Z'), - title: 'Old Customer Sync', - status: 'SCHEDULED', - recordingRequestStatus: 'REQUESTED', - calendarEventId: 'calendar-event-1', - externalBotId: 'recall-bot-1', - }, - ], - }); - - const result = await reconcileMeetingBotForCalendarEventIds({ - client: client as unknown as CoreApiClient, - calendarEventIds: ['calendar-event-1'], - now: NOW, - }); - - expect(result).toEqual([ - expect.objectContaining({ - action: 'UPDATED', - callRecordingId: buildCustomerSyncCallRecordingId( - '2026-01-01T11:30:00.000Z', - ), - }), - ]); - expect(client.callRecordings).toEqual([ - expect.objectContaining({ - title: 'Updated Customer Sync', - recordingRequestStatus: 'REQUESTED', - }), - ]); - }); - - it('updates an existing policy-managed scheduled call recording', async () => { - const client = buildFakeCoreApiClient({ - calendarEvents: [ - buildCalendarEvent({ - title: 'Updated Customer Sync', - }), - ], - callRecordings: [ - { - id: buildCustomerSyncCallRecordingId(), - title: 'Old Customer Sync', - status: 'SCHEDULED', - recordingRequestStatus: 'REQUESTED', - startedAt: FUTURE_STARTS_AT, - endedAt: FUTURE_ENDS_AT, - calendarEventId: 'calendar-event-1', - externalBotId: 'recall-bot-1', - }, - ], - }); - - const result = await reconcileMeetingBotForCalendarEventIds({ - client: client as unknown as CoreApiClient, - calendarEventIds: ['calendar-event-1'], - now: NOW, - }); - - expect(result).toEqual([ - expect.objectContaining({ - action: 'UPDATED', - callRecordingId: buildCustomerSyncCallRecordingId(), - }), - ]); - expect(client.callRecordings).toEqual([ - expect.objectContaining({ - id: buildCustomerSyncCallRecordingId(), - title: 'Updated Customer Sync', - status: 'SCHEDULED', - recordingRequestStatus: 'REQUESTED', - startedAt: FUTURE_STARTS_AT, - endedAt: FUTURE_ENDS_AT, - calendarEventId: 'calendar-event-1', - externalBotId: 'recall-bot-1', - }), - ]); - expect(rescheduleRecallBotMock).toHaveBeenCalledWith({ - externalBotId: 'recall-bot-1', - meetingUrl: 'https://meet.example.com/customer-sync', - joinAt: FUTURE_RECALL_BOT_JOIN_AT, - metadata: { - twentyWorkspaceId: WORKSPACE_ID, - twentyCallRecordingId: buildCustomerSyncCallRecordingId(), - twentyCalendarEventId: 'calendar-event-1', - twentyRealMeetingKey: - 'link:meet.example.com/customer-sync:2026-01-01T13:00:00.000Z', - }, - }); - }); - - it('cancels an existing scheduled request when the policy no longer requests a bot', async () => { - const client = buildFakeCoreApiClient({ - calendarEvents: [ - buildCalendarEvent({ - meetingBotPreference: 'OFF', - }), - ], - callRecordings: [ - { - id: 'call-recording-1', - title: 'Customer Sync', - status: 'SCHEDULED', - recordingRequestStatus: 'REQUESTED', - startedAt: FUTURE_STARTS_AT, - endedAt: FUTURE_ENDS_AT, - calendarEventId: 'calendar-event-1', - externalBotId: 'recall-bot-1', - }, - ], - }); - - const result = await reconcileMeetingBotForCalendarEventIds({ - client: client as unknown as CoreApiClient, - calendarEventIds: ['calendar-event-1'], - now: NOW, - }); - - expect(result).toEqual([ - expect.objectContaining({ - action: 'CANCELED', - callRecordingId: 'call-recording-1', - }), - ]); - expect(client.callRecordings).toEqual([ - expect.objectContaining({ - id: 'call-recording-1', - recordingRequestStatus: 'CANCELED', - externalBotId: null, - }), - ]); - expect(cancelRecallBotMock).toHaveBeenCalledWith({ - externalBotId: 'recall-bot-1', - }); - }); - - it('persists the cancel intent and leaves the bot for the planned stale-state cron when the Recall cancel fails', async () => { - cancelRecallBotMock.mockResolvedValue({ - ok: false, - status: 500, - errorMessage: 'Recall API responded with HTTP 500', - }); - - const client = buildFakeCoreApiClient({ - calendarEvents: [ - buildCalendarEvent({ - meetingBotPreference: 'OFF', - }), - ], - callRecordings: [ - { - id: 'call-recording-1', - title: 'Customer Sync', - status: 'SCHEDULED', - recordingRequestStatus: 'REQUESTED', - startedAt: FUTURE_STARTS_AT, - endedAt: FUTURE_ENDS_AT, - calendarEventId: 'calendar-event-1', - externalBotId: 'recall-bot-1', - }, - ], - }); - - const result = await reconcileMeetingBotForCalendarEventIds({ - client: client as unknown as CoreApiClient, - calendarEventIds: ['calendar-event-1'], - now: NOW, - }); - - expect(result).toEqual([ - expect.objectContaining({ - action: 'CANCELED', - callRecordingId: 'call-recording-1', - }), - ]); - expect(client.callRecordings).toEqual([ - expect.objectContaining({ - id: 'call-recording-1', - recordingRequestStatus: 'CANCELED', - externalBotId: 'recall-bot-1', - }), - ]); - }); - - it('does not reset the status of a recording whose bot is already live', async () => { - const client = buildFakeCoreApiClient({ - calendarEvents: [ - buildCalendarEvent({ - title: 'Renamed Customer Sync', - }), - ], - callRecordings: [ - { - id: buildCustomerSyncCallRecordingId(), - title: 'Customer Sync', - status: 'JOINING', - recordingRequestStatus: 'REQUESTED', - startedAt: FUTURE_STARTS_AT, - endedAt: FUTURE_ENDS_AT, - calendarEventId: 'calendar-event-1', - externalBotId: 'recall-bot-1', - }, - ], - }); - - const result = await reconcileMeetingBotForCalendarEventIds({ - client: client as unknown as CoreApiClient, - calendarEventIds: ['calendar-event-1'], - now: NOW, - }); - - expect(result).toEqual([ - expect.objectContaining({ - action: 'UPDATED', - callRecordingId: buildCustomerSyncCallRecordingId(), - }), - ]); - expect(client.callRecordings).toEqual([ - expect.objectContaining({ - id: buildCustomerSyncCallRecordingId(), - title: 'Renamed Customer Sync', - status: 'JOINING', - }), - ]); - }); - - it('creates a single recording when duplicate synced rows share the same real meeting', async () => { - const client = buildFakeCoreApiClient({ - calendarEvents: [ - buildCalendarEvent(), - buildCalendarEvent({ - id: 'calendar-event-2', - iCalUid: 'calendar-event-uid-from-other-channel', - }), - ], - }); - - const result = await reconcileMeetingBotForCalendarEventIds({ - client: client as unknown as CoreApiClient, - calendarEventIds: ['calendar-event-1'], - now: NOW, - }); - - expect(result).toEqual([ - expect.objectContaining({ - action: 'CREATED', - callRecordingId: buildCustomerSyncCallRecordingId(), - }), - ]); - expect(client.callRecordings).toHaveLength(1); - expect(scheduleRecallBotMock).toHaveBeenCalledTimes(1); - }); - - it('does not create a duplicate when a non-policy-managed open recording already exists', async () => { - const client = buildFakeCoreApiClient({ - calendarEvents: [buildCalendarEvent()], - callRecordings: [ - { - id: 'call-recording-1', - title: 'Manual Recording', - status: 'SCHEDULED', - recordingRequestStatus: null, - startedAt: FUTURE_STARTS_AT, - endedAt: FUTURE_ENDS_AT, - calendarEventId: 'calendar-event-1', - }, - ], - }); - - const result = await reconcileMeetingBotForCalendarEventIds({ - client: client as unknown as CoreApiClient, - calendarEventIds: ['calendar-event-1'], - now: NOW, - }); - - expect(result).toEqual([ - expect.objectContaining({ - action: 'SKIPPED', - callRecordingId: 'call-recording-1', - }), - ]); - expect(client.callRecordings).toHaveLength(1); - expect(client.mutations).toEqual([]); - expect(scheduleRecallBotMock).not.toHaveBeenCalled(); - }); - - it('cancels the scheduled request when the calendar event is deleted', async () => { - const client = buildFakeCoreApiClient({ - calendarEvents: [], - callRecordings: [ - { - id: 'call-recording-1', - title: 'Customer Sync', - status: 'SCHEDULED', - recordingRequestStatus: 'REQUESTED', - startedAt: FUTURE_STARTS_AT, - endedAt: FUTURE_ENDS_AT, - calendarEventId: 'calendar-event-1', - externalBotId: 'recall-bot-1', - }, - ], - }); - - const result = await reconcileMeetingBotForCalendarEventIds({ - client: client as unknown as CoreApiClient, - calendarEventIds: [], - removedOccurrences: [ - { - calendarEventId: 'calendar-event-1', - realMeetingKey: `link:meet.example.com/customer-sync:${FUTURE_STARTS_AT}`, - startsAt: FUTURE_STARTS_AT, - }, - ], - now: NOW, - }); - - expect(result).toEqual([ - expect.objectContaining({ - action: 'CANCELED', - callRecordingId: 'call-recording-1', - }), - ]); - expect(client.callRecordings).toEqual([ - expect.objectContaining({ - id: 'call-recording-1', - recordingRequestStatus: 'CANCELED', - externalBotId: null, - }), - ]); - expect(cancelRecallBotMock).toHaveBeenCalledWith({ - externalBotId: 'recall-bot-1', - }); - }); - - it('cancels the old occurrence and creates a fresh recording when the meeting moves to a new time', async () => { - const NEW_STARTS_AT = '2026-01-02T13:00:00.000Z'; - const NEW_RECALL_BOT_JOIN_AT = '2026-01-02T12:59:00.000Z'; - const NEW_ENDS_AT = '2026-01-02T14:00:00.000Z'; - const client = buildFakeCoreApiClient({ - calendarEvents: [ - buildCalendarEvent({ - startsAt: NEW_STARTS_AT, - endsAt: NEW_ENDS_AT, - }), - ], - callRecordings: [ - { - id: buildCustomerSyncCallRecordingId(), - title: 'Customer Sync', - status: 'SCHEDULED', - recordingRequestStatus: 'REQUESTED', - startedAt: FUTURE_STARTS_AT, - endedAt: FUTURE_ENDS_AT, - calendarEventId: 'calendar-event-1', - externalBotId: 'recall-bot-old', - }, - ], - }); - - const result = await reconcileMeetingBotForCalendarEventIds({ - client: client as unknown as CoreApiClient, - calendarEventIds: ['calendar-event-1'], - removedOccurrences: [ - { - calendarEventId: 'calendar-event-1', - realMeetingKey: `link:meet.example.com/customer-sync:${FUTURE_STARTS_AT}`, - startsAt: FUTURE_STARTS_AT, - }, - ], - now: NOW, - }); - - expect(result).toEqual([ - expect.objectContaining({ - action: 'CANCELED', - callRecordingId: buildCustomerSyncCallRecordingId(), - }), - expect.objectContaining({ - action: 'CREATED', - callRecordingId: buildCustomerSyncCallRecordingId(NEW_STARTS_AT), - }), - ]); - expect(cancelRecallBotMock).toHaveBeenCalledExactlyOnceWith({ - externalBotId: 'recall-bot-old', - }); - expect(scheduleRecallBotMock).toHaveBeenCalledExactlyOnceWith( - expect.objectContaining({ joinAt: NEW_RECALL_BOT_JOIN_AT }), - ); - expect(client.callRecordings).toEqual([ - expect.objectContaining({ - id: buildCustomerSyncCallRecordingId(), - recordingRequestStatus: 'CANCELED', - externalBotId: null, - }), - expect.objectContaining({ - id: buildCustomerSyncCallRecordingId(NEW_STARTS_AT), - recordingRequestStatus: 'REQUESTED', - externalBotId: 'recall-bot-1', - }), - ]); - }); - - it('reconciles the remaining meetings when one meeting fails', async () => { - cancelRecallBotMock.mockRejectedValue(new Error('recall exploded')); - - const client = buildFakeCoreApiClient({ - calendarEvents: [ - buildCalendarEvent({ - meetingBotPreference: 'OFF', - }), - buildCalendarEvent({ - id: 'calendar-event-2', - iCalUid: 'other-meeting-uid', - conferenceLink: { - primaryLinkUrl: 'https://meet.example.com/other-sync', - }, - }), - ], - callRecordings: [ - { - id: 'call-recording-1', - title: 'Customer Sync', - status: 'SCHEDULED', - recordingRequestStatus: 'REQUESTED', - startedAt: FUTURE_STARTS_AT, - endedAt: FUTURE_ENDS_AT, - calendarEventId: 'calendar-event-1', - externalBotId: 'recall-bot-1', - }, - ], - }); - - const result = await reconcileMeetingBotForCalendarEventIds({ - client: client as unknown as CoreApiClient, - calendarEventIds: ['calendar-event-1', 'calendar-event-2'], - now: NOW, - }); - - expect(result).toEqual([ - expect.objectContaining({ - action: 'FAILED', - realMeetingKey: `link:meet.example.com/customer-sync:${FUTURE_STARTS_AT}`, - errorMessage: 'recall exploded', - }), - expect.objectContaining({ action: 'CREATED' }), - ]); - expect(client.callRecordings).toEqual([ - expect.objectContaining({ - id: 'call-recording-1', - recordingRequestStatus: 'CANCELED', - externalBotId: 'recall-bot-1', - }), - expect.objectContaining({ - calendarEventId: 'calendar-event-2', - status: 'SCHEDULED', - }), - ]); - }); - - it('cancels the scheduled request when the conference link is removed', async () => { - const client = buildFakeCoreApiClient({ - calendarEvents: [ - buildCalendarEvent({ - conferenceLink: null, - }), - ], - callRecordings: [ - { - id: 'call-recording-1', - title: 'Customer Sync', - status: 'SCHEDULED', - recordingRequestStatus: 'REQUESTED', - startedAt: FUTURE_STARTS_AT, - endedAt: FUTURE_ENDS_AT, - calendarEventId: 'calendar-event-1', - externalBotId: 'recall-bot-1', - }, - ], - }); - - const result = await reconcileMeetingBotForCalendarEventIds({ - client: client as unknown as CoreApiClient, - calendarEventIds: ['calendar-event-1'], - now: NOW, - }); - - expect(result).toEqual([ - expect.objectContaining({ - action: 'CANCELED', - callRecordingId: 'call-recording-1', - }), - ]); - expect(client.callRecordings).toEqual([ - expect.objectContaining({ - id: 'call-recording-1', - recordingRequestStatus: 'CANCELED', - externalBotId: null, - }), - ]); - }); - - it('clears the stale bot id for the stale-state cron to re-create when the existing Recall bot no longer exists', async () => { - rescheduleRecallBotMock.mockResolvedValue({ - ok: false, - status: 404, - errorMessage: 'Recall API responded with HTTP 404', - }); - - const client = buildFakeCoreApiClient({ - calendarEvents: [buildCalendarEvent()], - callRecordings: [ - { - id: buildCustomerSyncCallRecordingId(), - title: 'Customer Sync', - status: 'SCHEDULED', - recordingRequestStatus: 'REQUESTED', - startedAt: FUTURE_STARTS_AT, - endedAt: FUTURE_ENDS_AT, - calendarEventId: 'calendar-event-1', - externalBotId: 'recall-bot-stale', - }, - ], - }); - - const result = await reconcileMeetingBotForCalendarEventIds({ - client: client as unknown as CoreApiClient, - calendarEventIds: ['calendar-event-1'], - now: NOW, - }); - - expect(result).toEqual([ - expect.objectContaining({ - action: 'UPDATED', - callRecordingId: buildCustomerSyncCallRecordingId(), - }), - ]); - expect(rescheduleRecallBotMock).toHaveBeenCalledWith( - expect.objectContaining({ externalBotId: 'recall-bot-stale' }), - ); - // The event path no longer re-creates the bot; the stale id is cleared and the cron heals the botless row. - expect(scheduleRecallBotMock).not.toHaveBeenCalled(); - expect(client.callRecordings).toEqual([ - expect.objectContaining({ - id: buildCustomerSyncCallRecordingId(), - externalBotId: null, - }), - ]); - }); - - it('adopts the concurrently created recording when it loses the deterministic-id insert race', async () => { - class InsertRaceFakeCoreApiClient extends FakeCoreApiClient { - override async mutation(mutation: any): Promise { - if (mutation.createCallRecording !== undefined) { - const concurrentlyInsertedId = - mutation.createCallRecording.__args.data.id; - - if ( - !this.callRecordings.some( - (candidate) => candidate.id === concurrentlyInsertedId, - ) - ) { - this.callRecordings.push({ - id: concurrentlyInsertedId, - status: 'SCHEDULED', - recordingRequestStatus: 'REQUESTED', - calendarEventId: 'calendar-event-1', - externalBotId: 'sibling-bot', - }); - } - } - - return super.mutation(mutation); - } - } - - const client = new InsertRaceFakeCoreApiClient({ - calendarEvents: [buildCalendarEvent()], - }); - - const result = await reconcileMeetingBotForCalendarEventIds({ - client: client as unknown as CoreApiClient, - calendarEventIds: ['calendar-event-1'], - now: NOW, - }); - - expect(result).toEqual([ - expect.objectContaining({ - action: 'UPDATED', - callRecordingId: buildCustomerSyncCallRecordingId(), - }), - ]); - expect(client.callRecordings).toHaveLength(1); - expect(scheduleRecallBotMock).not.toHaveBeenCalled(); - expect(rescheduleRecallBotMock).toHaveBeenCalledWith( - expect.objectContaining({ externalBotId: 'sibling-bot' }), - ); - }); - - it('fails the meeting when the create conflicts without a readable recording', async () => { - class TombstoneFakeCoreApiClient extends FakeCoreApiClient { - override async mutation(mutation: any): Promise { - if (mutation.createCallRecording !== undefined) { - throw new Error('Duplicate id on a soft-deleted record'); - } - - return super.mutation(mutation); - } - } - - const client = new TombstoneFakeCoreApiClient({ - calendarEvents: [buildCalendarEvent()], - }); - - const result = await reconcileMeetingBotForCalendarEventIds({ - client: client as unknown as CoreApiClient, - calendarEventIds: ['calendar-event-1'], - now: NOW, - }); - - expect(result).toEqual([ - expect.objectContaining({ - action: 'FAILED', - errorMessage: 'Duplicate id on a soft-deleted record', - }), - ]); - expect(scheduleRecallBotMock).not.toHaveBeenCalled(); - }); - - it('schedules exactly one bot when concurrent reconciles race for the same meeting', async () => { - const client = buildFakeCoreApiClient({ - calendarEvents: [buildCalendarEvent()], - }); - - await Promise.all( - Array.from({ length: 4 }, () => - reconcileMeetingBotForCalendarEventIds({ - client: client as unknown as CoreApiClient, - calendarEventIds: ['calendar-event-1'], - now: NOW, - }), - ), - ); - - expect(client.callRecordings).toHaveLength(1); - expect(scheduleRecallBotMock).toHaveBeenCalledTimes(1); - expect(client.callRecordings[0].externalBotId).toBe('recall-bot-1'); - }); - - it('does not schedule a bot when the recording is canceled between decide and schedule', async () => { - class CancelRaceFakeCoreApiClient extends FakeCoreApiClient { - override async query(query: any): Promise { - if (query.callRecordings?.__args.filter.id?.in !== undefined) { - const callRecording = this.callRecordings.find((candidate) => - query.callRecordings.__args.filter.id.in.includes(candidate.id), - ); - - if (callRecording !== undefined) { - callRecording.recordingRequestStatus = 'CANCELED'; - } - } - - return super.query(query); - } - } - - const client = new CancelRaceFakeCoreApiClient({ - calendarEvents: [buildCalendarEvent()], - }); - - const result = await reconcileMeetingBotForCalendarEventIds({ - client: client as unknown as CoreApiClient, - calendarEventIds: ['calendar-event-1'], - now: NOW, - }); - - expect(result).toEqual([expect.objectContaining({ action: 'CREATED' })]); - expect(scheduleRecallBotMock).not.toHaveBeenCalled(); - }); -}); diff --git a/packages/twenty-apps/public/twenty-meeting-bot/src/logic-functions/flows/cancel-call-recording-request.util.ts b/packages/twenty-apps/public/twenty-meeting-bot/src/logic-functions/flows/cancel-call-recording-request.util.ts deleted file mode 100644 index 8184955a534f9..0000000000000 --- a/packages/twenty-apps/public/twenty-meeting-bot/src/logic-functions/flows/cancel-call-recording-request.util.ts +++ /dev/null @@ -1,46 +0,0 @@ -import { isUndefined } from '@sniptt/guards'; -import { type CoreApiClient } from 'twenty-client-sdk/core'; - -import { CallRecordingRequestStatus } from 'src/logic-functions/constants/call-recording-request-status'; -import { type CallRecordingRecord } from 'src/logic-functions/types/call-recording-record.type'; -import { cancelRecallBot } from 'src/logic-functions/recall-api/cancel-recall-bot.util'; -import { updateCallRecording } from 'src/logic-functions/data/update-call-recording.util'; - -// Intent-first: the stale-state cron finishes the Recall half when this call fails. -export const cancelCallRecordingRequest = async ({ - client, - callRecording, -}: { - client: CoreApiClient; - callRecording: CallRecordingRecord; -}): Promise => { - await updateCallRecording(client, { - id: callRecording.id, - data: { - recordingRequestStatus: CallRecordingRequestStatus.CANCELED, - }, - }); - - if (isUndefined(callRecording.externalBotId)) { - return; - } - - const cancelResult = await cancelRecallBot({ - externalBotId: callRecording.externalBotId, - }); - - if (!cancelResult.ok) { - console.warn( - `[twenty-meeting-bot] failed to cancel Recall bot for callRecording ${callRecording.id}, leaving it for the stale-state cron: ${cancelResult.errorMessage}`, - ); - - return; - } - - await updateCallRecording(client, { - id: callRecording.id, - data: { - externalBotId: null, - }, - }); -}; diff --git a/packages/twenty-apps/public/twenty-meeting-bot/src/logic-functions/flows/charge-completed-call-recording.util.ts b/packages/twenty-apps/public/twenty-meeting-bot/src/logic-functions/flows/charge-completed-call-recording.util.ts deleted file mode 100644 index dd1a02be0f8dc..0000000000000 --- a/packages/twenty-apps/public/twenty-meeting-bot/src/logic-functions/flows/charge-completed-call-recording.util.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { isUndefined } from '@sniptt/guards'; -import { chargeCredits } from 'twenty-sdk/billing'; - -import { computeCallRecordingCharge } from 'src/logic-functions/domain/compute-call-recording-charge.util'; - -export const chargeCompletedCallRecording = async ({ - callRecordingId, - startedAt, - endedAt, -}: { - callRecordingId: string; - startedAt: string | undefined; - endedAt: string | undefined; -}): Promise => { - const charge = computeCallRecordingCharge({ startedAt, endedAt }); - - if (isUndefined(charge)) { - console.warn( - `[twenty-meeting-bot] call recording ${callRecordingId} completed without usable recording timestamps; it will not be billed`, - ); - - return; - } - - await chargeCredits({ - creditsUsedMicro: charge.creditsUsedMicro, - quantity: charge.quantityMinutes, - operationType: 'CALL_RECORDING', - resourceContext: 'recall', - }); -}; diff --git a/packages/twenty-apps/public/twenty-meeting-bot/src/logic-functions/flows/complete-and-charge-call-recording.util.ts b/packages/twenty-apps/public/twenty-meeting-bot/src/logic-functions/flows/complete-and-charge-call-recording.util.ts deleted file mode 100644 index 1c1dff964b80c..0000000000000 --- a/packages/twenty-apps/public/twenty-meeting-bot/src/logic-functions/flows/complete-and-charge-call-recording.util.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { type CoreApiClient } from 'twenty-client-sdk/core'; - -import { completeCallRecordingIngestion } from 'src/logic-functions/data/complete-call-recording-ingestion.util'; -import { chargeCompletedCallRecording } from 'src/logic-functions/flows/charge-completed-call-recording.util'; - -export const completeAndChargeCallRecording = async ( - client: CoreApiClient, - { - id, - startedAt, - endedAt, - }: { - id: string; - startedAt: string | undefined; - endedAt: string | undefined; - }, -): Promise => { - const claimed = await completeCallRecordingIngestion(client, { id }); - - if (claimed) { - await chargeCompletedCallRecording({ - callRecordingId: id, - startedAt, - endedAt, - }); - } - - return claimed; -}; diff --git a/packages/twenty-apps/public/twenty-meeting-bot/src/logic-functions/flows/converge-diverged-call-recordings-result.type.ts b/packages/twenty-apps/public/twenty-meeting-bot/src/logic-functions/flows/converge-diverged-call-recordings-result.type.ts deleted file mode 100644 index d76740720d778..0000000000000 --- a/packages/twenty-apps/public/twenty-meeting-bot/src/logic-functions/flows/converge-diverged-call-recordings-result.type.ts +++ /dev/null @@ -1,8 +0,0 @@ -export type ConvergeDivergedCallRecordingsResult = { - candidateCount: number; - updatedCallRecordingIds: string[]; - markedFailedCallRecordingIds: string[]; - requestedTranscriptCallRecordingIds: string[]; - unconvergeableCallRecordingIds: string[]; - skippedNotStartedCallRecordingIds: string[]; -}; diff --git a/packages/twenty-apps/public/twenty-meeting-bot/src/logic-functions/flows/converge-diverged-call-recordings.util.ts b/packages/twenty-apps/public/twenty-meeting-bot/src/logic-functions/flows/converge-diverged-call-recordings.util.ts deleted file mode 100644 index 0e27173f4e418..0000000000000 --- a/packages/twenty-apps/public/twenty-meeting-bot/src/logic-functions/flows/converge-diverged-call-recordings.util.ts +++ /dev/null @@ -1,449 +0,0 @@ -import { isNonEmptyArray, isUndefined } from '@sniptt/guards'; -import { type CoreApiClient } from 'twenty-client-sdk/core'; - -import { CallRecordingRequestStatus } from 'src/logic-functions/constants/call-recording-request-status'; -import { CallRecordingStatus } from 'src/logic-functions/constants/call-recording-status'; -import { NON_TERMINAL_CALL_RECORDING_STATUSES } from 'src/logic-functions/constants/non-terminal-call-recording-statuses'; -import { TWENTY_PAGE_SIZE } from 'src/logic-functions/constants/twenty-page-size'; -import { type FilesFieldValue } from 'src/logic-functions/types/files-field-value.type'; -import { - extractRecallBotConvergence, - type RecallBotConvergence, -} from 'src/logic-functions/recall-api/extract-recall-bot-convergence.util'; -import { - fetchAllNodes, - type ConnectionPage, -} from 'src/logic-functions/data/fetch-all-nodes.util'; -import { getRecallBot } from 'src/logic-functions/recall-api/get-recall-bot.util'; -import { ingestCallRecordingMedia } from 'src/logic-functions/flows/ingest-call-recording-media.util'; -import { isCallRecordingStatusDowngrade } from 'src/logic-functions/domain/is-call-recording-status-downgrade.util'; -import { isNonEmptyString } from 'src/logic-functions/utils/is-non-empty-string.util'; -import { parseTranscriptMarker } from 'src/logic-functions/domain/parse-transcript-marker.util'; -import { persistCallRecordingProgress } from 'src/logic-functions/flows/persist-call-recording-progress.util'; -import { reconcileCallRecordingTranscriptArtifact } from 'src/logic-functions/flows/reconcile-call-recording-transcript-artifact.util'; -import { type ConvergeDivergedCallRecordingsResult } from 'src/logic-functions/flows/converge-diverged-call-recordings-result.type'; -import { shouldCompleteCallRecordingIngestion } from 'src/logic-functions/domain/should-complete-call-recording-ingestion.util'; -import { - updateCallRecording, - type CallRecordingUpdateFields, -} from 'src/logic-functions/data/update-call-recording.util'; - -const CONVERGENCE_LOOKBACK_DAYS = 7; - -type DivergedCallRecordingCandidate = { - id: string; - status: string | undefined; - startedAt: string | undefined; - endedAt: string | undefined; - externalBotId: string | undefined; - externalRecordingId: string | undefined; - transcript: unknown; - audio: FilesFieldValue | undefined; - video: FilesFieldValue | undefined; - createdAt: string | undefined; - calendarEventStartsAt: string | undefined; - calendarEventEndsAt: string | undefined; -}; - -type DivergedCallRecordingNode = { - id: string; - status?: string | null; - startedAt?: string | null; - endedAt?: string | null; - externalBotId?: string | null; - externalRecordingId?: string | null; - transcript?: unknown; - audio?: FilesFieldValue | null; - video?: FilesFieldValue | null; - createdAt?: string | null; - calendarEvent?: { startsAt?: string | null; endsAt?: string | null } | null; -}; - -// Webhook deliveries get lost; this pull pass re-derives state from Recall. -export const convergeDivergedCallRecordings = async ({ - client, - now, -}: { - client: CoreApiClient; - now: Date; -}): Promise => { - const candidates = await fetchDivergedCallRecordingCandidates(client); - const convergenceLowerBound = new Date( - now.getTime() - CONVERGENCE_LOOKBACK_DAYS * 24 * 60 * 60 * 1000, - ); - const result: ConvergeDivergedCallRecordingsResult = { - candidateCount: candidates.length, - updatedCallRecordingIds: [], - markedFailedCallRecordingIds: [], - requestedTranscriptCallRecordingIds: [], - unconvergeableCallRecordingIds: [], - skippedNotStartedCallRecordingIds: [], - }; - - for (const candidate of candidates) { - if (isOutsideConvergenceBound(candidate, convergenceLowerBound)) { - console.warn( - `[twenty-meeting-bot] call recording ${candidate.id} diverged but its meeting ended more than ${CONVERGENCE_LOOKBACK_DAYS} days ago; it will not converge automatically`, - ); - result.unconvergeableCallRecordingIds.push(candidate.id); - continue; - } - - if (isUndefined(candidate.externalBotId)) { - console.warn( - `[twenty-meeting-bot] call recording ${candidate.id} diverged but has no Recall bot id; it will not converge automatically`, - ); - result.unconvergeableCallRecordingIds.push(candidate.id); - continue; - } - - if (isBeforeMeetingStart(candidate, now)) { - result.skippedNotStartedCallRecordingIds.push(candidate.id); - continue; - } - - await convergeCallRecording({ - client, - candidate, - externalBotId: candidate.externalBotId, - now, - result, - }); - } - - return result; -}; - -const fetchDivergedCallRecordingCandidates = async ( - client: CoreApiClient, -): Promise => { - // No createdAt bound: older-than-lookback candidates must surface in logs. - const filter: Record = { - or: [ - { - recordingRequestStatus: { eq: CallRecordingRequestStatus.REQUESTED }, - status: { in: NON_TERMINAL_CALL_RECORDING_STATUSES }, - externalBotId: { is: 'NOT_NULL' }, - }, - { - status: { eq: CallRecordingStatus.COMPLETED }, - or: [{ startedAt: { is: 'NULL' } }, { endedAt: { is: 'NULL' } }], - }, - ], - }; - const candidateNodes = await fetchAllNodes( - async (afterCursor) => { - const queryResult = await client.query({ - callRecordings: { - __args: { - filter, - first: TWENTY_PAGE_SIZE, - ...(isUndefined(afterCursor) ? {} : { after: afterCursor }), - }, - pageInfo: { - hasNextPage: true, - endCursor: true, - }, - edges: { - node: { - id: true, - status: true, - startedAt: true, - endedAt: true, - externalBotId: true, - externalRecordingId: true, - transcript: true, - audio: { fileId: true }, - video: { fileId: true }, - createdAt: true, - calendarEvent: { - startsAt: true, - endsAt: true, - }, - }, - }, - }, - }); - - return (queryResult.callRecordings ?? undefined) as - | ConnectionPage - | undefined; - }, - ); - - return candidateNodes.map((node) => ({ - id: node.id, - status: node.status ?? undefined, - startedAt: node.startedAt ?? undefined, - endedAt: node.endedAt ?? undefined, - externalBotId: isNonEmptyString(node.externalBotId) - ? node.externalBotId - : undefined, - externalRecordingId: isNonEmptyString(node.externalRecordingId) - ? node.externalRecordingId - : undefined, - transcript: node.transcript ?? undefined, - audio: node.audio ?? undefined, - video: node.video ?? undefined, - createdAt: node.createdAt ?? undefined, - calendarEventStartsAt: node.calendarEvent?.startsAt ?? undefined, - calendarEventEndsAt: node.calendarEvent?.endsAt ?? undefined, - })); -}; - -// Anchored to meeting end: createdAt is scheduling time and can predate the meeting by weeks. -const isOutsideConvergenceBound = ( - candidate: DivergedCallRecordingCandidate, - convergenceLowerBound: Date, -): boolean => { - const meetingEndReference = - candidate.calendarEventEndsAt ?? candidate.createdAt; - - return ( - !isUndefined(meetingEndReference) && - new Date(meetingEndReference).getTime() < convergenceLowerBound.getTime() - ); -}; - -// Until the meeting starts the bot has recorded nothing, so there is nothing to pull yet. -const isBeforeMeetingStart = ( - candidate: DivergedCallRecordingCandidate, - now: Date, -): boolean => - !isUndefined(candidate.calendarEventStartsAt) && - new Date(candidate.calendarEventStartsAt).getTime() > now.getTime(); - -const convergeCallRecording = async ({ - client, - candidate, - externalBotId, - now, - result, -}: { - client: CoreApiClient; - candidate: DivergedCallRecordingCandidate; - externalBotId: string; - now: Date; - result: ConvergeDivergedCallRecordingsResult; -}): Promise => { - const botResult = await getRecallBot({ externalBotId }); - - if (!botResult.ok) { - if (botResult.status === 404) { - await markCallRecordingFailedAfterBotLoss({ - client, - candidate, - externalBotId, - result, - }); - - return; - } - - console.warn( - `[twenty-meeting-bot] failed to fetch Recall bot ${externalBotId} for call recording ${candidate.id}: ${botResult.errorMessage}`, - ); - - return; - } - - const convergence = extractRecallBotConvergence(botResult.bot); - const updateData = buildConvergenceFieldUpdates({ candidate, convergence }); - - const externalRecordingId = - candidate.externalRecordingId ?? convergence.externalRecordingId; - - if (convergence.isRecallRecordingDone && !isUndefined(externalRecordingId)) { - const transcriptArtifactResult = - await reconcileCallRecordingTranscriptArtifact({ - callRecordingId: candidate.id, - currentStatus: candidate.status, - externalRecordingId, - requestedAt: now.toISOString(), - transcript: candidate.transcript, - }); - - Object.assign(updateData, transcriptArtifactResult.updateData); - - if (transcriptArtifactResult.requestedTranscript) { - result.requestedTranscriptCallRecordingIds.push(candidate.id); - } - - Object.assign( - updateData, - await ingestCallRecordingMedia({ - callRecordingId: candidate.id, - externalRecordingId, - hasAudio: isNonEmptyArray(candidate.audio), - hasVideo: isNonEmptyArray(candidate.video), - }), - ); - } - - const terminalArtifactGateFailureUpdate = - buildTerminalArtifactGateFailureUpdate({ - candidate, - convergence, - externalRecordingId, - updateData, - }); - - if (!isUndefined(terminalArtifactGateFailureUpdate)) { - Object.assign(updateData, terminalArtifactGateFailureUpdate); - } - - const completesIngestion = shouldCompleteCallRecordingIngestion({ - current: candidate, - updateData, - }); - - if (Object.keys(updateData).length === 0 && !completesIngestion) { - return; - } - - await persistCallRecordingProgress(client, { - id: candidate.id, - current: candidate, - updateData, - }); - - result.updatedCallRecordingIds.push(candidate.id); -}; - -// Pure merge: fill only unset candidate fields and never downgrade status. -const buildConvergenceFieldUpdates = ({ - candidate, - convergence, -}: { - candidate: DivergedCallRecordingCandidate; - convergence: RecallBotConvergence; -}): CallRecordingUpdateFields => { - const updateData: CallRecordingUpdateFields = {}; - - if ( - !isUndefined(convergence.status) && - convergence.status !== candidate.status && - !isCallRecordingStatusDowngrade({ - fromStatus: candidate.status, - toStatus: convergence.status, - }) - ) { - updateData.status = convergence.status; - - if (convergence.status === CallRecordingStatus.FAILED) { - updateData.meetingBotFailureReason = - convergence.failureReason ?? 'recall_bot_failed'; - } - } - - if (isUndefined(candidate.startedAt) && !isUndefined(convergence.startedAt)) { - updateData.startedAt = convergence.startedAt; - } - - if (isUndefined(candidate.endedAt) && !isUndefined(convergence.endedAt)) { - updateData.endedAt = convergence.endedAt; - } - - if ( - isUndefined(candidate.externalRecordingId) && - !isUndefined(convergence.externalRecordingId) - ) { - updateData.externalRecordingId = convergence.externalRecordingId; - } - - return updateData; -}; - -type TerminalArtifactGateFailureUpdate = { - status: CallRecordingStatus.FAILED; - meetingBotFailureReason: string; -}; - -const buildTerminalArtifactGateFailureUpdate = ({ - candidate, - convergence, - externalRecordingId, - updateData, -}: { - candidate: DivergedCallRecordingCandidate; - convergence: RecallBotConvergence; - externalRecordingId: string | undefined; - updateData: CallRecordingUpdateFields; -}): TerminalArtifactGateFailureUpdate | undefined => { - if ( - candidate.status === CallRecordingStatus.COMPLETED || - updateData.status === CallRecordingStatus.FAILED || - !convergence.isRecallRecordingDone || - !isUndefined(externalRecordingId) || - hasRecordingArtifactPath({ candidate, updateData }) - ) { - return undefined; - } - - return { - status: CallRecordingStatus.FAILED, - meetingBotFailureReason: - convergence.failureReason ?? 'recording_artifacts_unavailable', - }; -}; - -const hasRecordingArtifactPath = ({ - candidate, - updateData, -}: { - candidate: DivergedCallRecordingCandidate; - updateData: CallRecordingUpdateFields; -}): boolean => { - return ( - isNonEmptyArray(updateData.audio ?? candidate.audio) || - isNonEmptyArray(updateData.video ?? candidate.video) || - hasReachableTranscript(updateData.transcript ?? candidate.transcript) - ); -}; - -const hasReachableTranscript = (transcript: unknown): boolean => { - if (isUndefined(transcript)) { - return false; - } - - const marker = parseTranscriptMarker(transcript); - - return isUndefined(marker) || marker.status === 'PENDING'; -}; - -const markCallRecordingFailedAfterBotLoss = async ({ - client, - candidate, - externalBotId, - result, -}: { - client: CoreApiClient; - candidate: DivergedCallRecordingCandidate; - externalBotId: string; - result: ConvergeDivergedCallRecordingsResult; -}): Promise => { - // externalBotId is kept for audit even though the bot is gone at Recall. - console.warn( - `[twenty-meeting-bot] Recall bot ${externalBotId} for call recording ${candidate.id} no longer exists; it will not converge automatically`, - ); - - if ( - isCallRecordingStatusDowngrade({ - fromStatus: candidate.status, - toStatus: CallRecordingStatus.FAILED, - }) - ) { - result.unconvergeableCallRecordingIds.push(candidate.id); - - return; - } - - await updateCallRecording(client, { - id: candidate.id, - data: { - status: CallRecordingStatus.FAILED, - meetingBotFailureReason: 'recall_bot_not_found', - }, - }); - result.markedFailedCallRecordingIds.push(candidate.id); -}; diff --git a/packages/twenty-apps/public/twenty-meeting-bot/src/logic-functions/flows/download-transcript.util.ts b/packages/twenty-apps/public/twenty-meeting-bot/src/logic-functions/flows/download-transcript.util.ts deleted file mode 100644 index f4a69ea813854..0000000000000 --- a/packages/twenty-apps/public/twenty-meeting-bot/src/logic-functions/flows/download-transcript.util.ts +++ /dev/null @@ -1,67 +0,0 @@ -import { isUndefined } from '@sniptt/guards'; - -import { retrieveRecallTranscript } from 'src/logic-functions/recall-api/retrieve-recall-transcript.util'; - -const TRANSCRIPT_DOWNLOAD_TIMEOUT_MS = 20_000; - -export type DownloadTranscriptResult = - | { outcome: 'filled'; content: unknown } - | { outcome: 'failed'; subCode: string | null } - | { outcome: 'pending' } - | { outcome: 'error'; errorMessage: string }; - -export const downloadTranscript = async ({ - transcriptId, -}: { - transcriptId: string; -}): Promise => { - const retrieveResult = await retrieveRecallTranscript({ transcriptId }); - - if (!retrieveResult.ok) { - return { outcome: 'error', errorMessage: retrieveResult.errorMessage }; - } - - const { downloadUrl, statusCode, statusSubCode } = retrieveResult.transcript; - - if (!isUndefined(downloadUrl)) { - return downloadTranscriptContent(downloadUrl); - } - - if (statusCode === 'error' || statusCode === 'failed') { - return { outcome: 'failed', subCode: statusSubCode ?? null }; - } - - return { outcome: 'pending' }; -}; - -const downloadTranscriptContent = async ( - downloadUrl: string, -): Promise => { - try { - const response = await fetch(downloadUrl, { - signal: AbortSignal.timeout(TRANSCRIPT_DOWNLOAD_TIMEOUT_MS), - }); - - if (!response.ok) { - console.warn( - `[twenty-meeting-bot] transcript download responded with HTTP ${response.status}`, - ); - - return { - outcome: 'error', - errorMessage: 'transcript download failed', - }; - } - - return { outcome: 'filled', content: await response.json() }; - } catch (error) { - console.warn( - `[twenty-meeting-bot] transcript download failed: ${error instanceof Error ? error.message : String(error)}`, - ); - - return { - outcome: 'error', - errorMessage: 'transcript download failed', - }; - } -}; diff --git a/packages/twenty-apps/public/twenty-meeting-bot/src/logic-functions/flows/ensure-meeting-bot.util.ts b/packages/twenty-apps/public/twenty-meeting-bot/src/logic-functions/flows/ensure-meeting-bot.util.ts deleted file mode 100644 index 165e9a88ae8cc..0000000000000 --- a/packages/twenty-apps/public/twenty-meeting-bot/src/logic-functions/flows/ensure-meeting-bot.util.ts +++ /dev/null @@ -1,74 +0,0 @@ -import { isUndefined } from '@sniptt/guards'; -import { type CoreApiClient } from 'twenty-client-sdk/core'; - -import { CallRecordingRequestStatus } from 'src/logic-functions/constants/call-recording-request-status'; -import { type MeetingRecording } from 'src/logic-functions/types/meeting-recording.type'; -import { buildRecallBotMetadata } from 'src/logic-functions/domain/build-recall-bot-metadata.util'; -import { computeRecallBotJoinAt } from 'src/logic-functions/domain/compute-recall-bot-join-at.util'; -import { findCallRecordingsByIds } from 'src/logic-functions/data/find-call-recordings-by-ids.util'; -import { getCurrentWorkspaceId } from 'src/logic-functions/data/get-current-workspace-id.util'; -import { scheduleRecallBot } from 'src/logic-functions/recall-api/schedule-recall-bot.util'; -import { updateCallRecording } from 'src/logic-functions/data/update-call-recording.util'; - -// The sole place a Recall bot is created. Only the deterministic-create winner and the stale-state cron call it, so one writer per meeting POSTs exactly one bot. -export const ensureMeetingBot = async ( - client: CoreApiClient, - { callRecording, calendarEvent }: MeetingRecording, -): Promise => { - const meetingUrl = calendarEvent.conferenceLinkUrl; - const meetingStartsAt = calendarEvent.startsAt; - - if (isUndefined(meetingUrl) || isUndefined(meetingStartsAt)) { - return false; - } - - const joinAt = computeRecallBotJoinAt(meetingStartsAt); - - const freshCallRecording = ( - await findCallRecordingsByIds(client, [callRecording.id]) - )[0]; - - if ( - isUndefined(freshCallRecording) || - freshCallRecording.recordingRequestStatus !== - CallRecordingRequestStatus.REQUESTED || - !isUndefined(freshCallRecording.externalBotId) - ) { - return false; - } - - const workspaceId = getCurrentWorkspaceId(); - - if (isUndefined(workspaceId)) { - console.error( - `[twenty-meeting-bot] cannot schedule Recall bot for callRecording ${callRecording.id}: workspace id unavailable, the shared webhook could not be routed back`, - ); - - return false; - } - - const scheduleResult = await scheduleRecallBot({ - meetingUrl, - joinAt, - metadata: buildRecallBotMetadata({ - callRecording, - calendarEvent, - workspaceId, - }), - }); - - if (!scheduleResult.ok) { - console.warn( - `[twenty-meeting-bot] failed to schedule Recall bot for callRecording ${callRecording.id}: ${scheduleResult.errorMessage}`, - ); - - return false; - } - - await updateCallRecording(client, { - id: callRecording.id, - data: { externalBotId: scheduleResult.externalBotId }, - }); - - return true; -}; diff --git a/packages/twenty-apps/public/twenty-meeting-bot/src/logic-functions/flows/handle-recall-webhook.util.ts b/packages/twenty-apps/public/twenty-meeting-bot/src/logic-functions/flows/handle-recall-webhook.util.ts deleted file mode 100644 index 3ef4ed12af76f..0000000000000 --- a/packages/twenty-apps/public/twenty-meeting-bot/src/logic-functions/flows/handle-recall-webhook.util.ts +++ /dev/null @@ -1,673 +0,0 @@ -import { isNonEmptyArray, isNull, isUndefined } from '@sniptt/guards'; -import { type CoreApiClient } from 'twenty-client-sdk/core'; - -import { CallRecordingStatus } from 'src/logic-functions/constants/call-recording-status'; -import { type FilesFieldValue } from 'src/logic-functions/types/files-field-value.type'; -import { buildFailedTranscriptMarker } from 'src/logic-functions/domain/build-failed-transcript-marker.util'; -import { buildTranscriptFailureReason } from 'src/logic-functions/domain/build-transcript-failure-reason.util'; -import { downloadTranscript } from 'src/logic-functions/flows/download-transcript.util'; -import { extractRecallBotConvergence } from 'src/logic-functions/recall-api/extract-recall-bot-convergence.util'; -import { getRecallBot } from 'src/logic-functions/recall-api/get-recall-bot.util'; -import { getString } from 'src/logic-functions/utils/get-string.util'; -import { ingestCallRecordingMedia } from 'src/logic-functions/flows/ingest-call-recording-media.util'; -import { isCallRecordingStatusDowngrade } from 'src/logic-functions/domain/is-call-recording-status-downgrade.util'; -import { isRecallRecordingDoneSignal } from 'src/logic-functions/domain/is-recall-recording-done-signal.util'; -import { mapRecallStatusCodeToCallRecordingStatus } from 'src/logic-functions/domain/map-recall-status-code-to-call-recording-status.util'; -import { - parseRecallWebhookEvent, - type RecallWebhookBody, - type RecallWebhookEvent, -} from 'src/logic-functions/recall-api/parse-recall-webhook-event.util'; -import { parseTranscriptMarker } from 'src/logic-functions/domain/parse-transcript-marker.util'; -import { persistCallRecordingProgress } from 'src/logic-functions/flows/persist-call-recording-progress.util'; -import { reconcileCallRecordingTranscriptArtifact } from 'src/logic-functions/flows/reconcile-call-recording-transcript-artifact.util'; -import { - updateCallRecording, - type CallRecordingUpdateFields, -} from 'src/logic-functions/data/update-call-recording.util'; - -type MatchedCallRecording = { - id: string; - status?: string; - startedAt?: string; - endedAt?: string; - externalRecordingId?: string; - transcript?: unknown; - audio?: FilesFieldValue; - video?: FilesFieldValue; -}; - -type ExternalRecordingIdResolution = { - externalRecordingId: string | undefined; - providerLookupFailed: boolean; -}; - -type RecallWebhookHandlerResult = - | { - status: 'updated'; - callRecordingId: string; - event: string; - callRecordingStatus: string; - } - | { - status: 'updated'; - callRecordingId: string; - event: string; - transcriptOutcome: 'FILLED' | 'FAILED'; - } - | { - status: 'skipped'; - event: string | null; - reason: string; - }; - -export const handleRecallWebhook = async ({ - client, - body, -}: { - client: CoreApiClient; - body: RecallWebhookBody; -}): Promise => { - const webhookEvent = parseRecallWebhookEvent(body); - - if (isUndefined(webhookEvent)) { - return { - status: 'skipped', - event: null, - reason: 'missing event type', - }; - } - - const { event } = webhookEvent; - - if (event === 'transcript.done' || event === 'transcript.failed') { - return handleRecallTranscriptEvent({ client, webhookEvent, event }); - } - - return handleRecallStatusEvent({ client, webhookEvent }); -}; - -const handleRecallStatusEvent = async ({ - client, - webhookEvent, -}: { - client: CoreApiClient; - webhookEvent: RecallWebhookEvent; -}): Promise => { - const { event, statusCode } = webhookEvent; - const callRecordingStatus = mapRecallEventToCallRecordingStatus({ - event, - statusCode, - }); - - if (isUndefined(callRecordingStatus)) { - return { - status: 'skipped', - event, - reason: `unsupported Recall event status ${statusCode ?? event}`, - }; - } - - const callRecording = await findMatchingCallRecording({ - client, - webhookEvent, - }); - - if (isUndefined(callRecording)) { - return { - status: 'skipped', - event, - reason: 'no matching call recording', - }; - } - - if ( - isCallRecordingStatusDowngrade({ - fromStatus: callRecording.status, - toStatus: callRecordingStatus, - }) - ) { - return { - status: 'skipped', - event, - reason: `stale status event (${callRecording.status} -> ${callRecordingStatus})`, - }; - } - - const updateData: CallRecordingUpdateFields = { - ...(isUndefined(webhookEvent.externalBotId) - ? {} - : { externalBotId: webhookEvent.externalBotId }), - ...buildExternalRecordingIdUpdate(webhookEvent), - ...buildCallRecordingStatusUpdate({ - reason: getRecallWebhookFailureReason(webhookEvent), - status: callRecordingStatus, - }), - ...buildRecordingTimestampsUpdate({ webhookEvent, callRecording }), - }; - - if (isRecallRecordingDoneSignal({ event, statusCode })) { - const externalRecordingIdResolution = await resolveExternalRecordingId({ - callRecording, - webhookEvent, - }); - - Object.assign( - updateData, - await buildTranscriptArtifactUpdate({ - callRecording, - externalRecordingId: externalRecordingIdResolution.externalRecordingId, - }), - ); - - Object.assign( - updateData, - await buildMediaIngestionUpdate({ - callRecording, - externalRecordingId: externalRecordingIdResolution.externalRecordingId, - }), - ); - - const terminalArtifactGateFailureUpdate = - buildTerminalArtifactGateFailureUpdate({ - callRecording, - providerLookupFailed: - externalRecordingIdResolution.providerLookupFailed, - updateData, - webhookEvent, - }); - - if (!isUndefined(terminalArtifactGateFailureUpdate)) { - Object.assign(updateData, terminalArtifactGateFailureUpdate); - } - } - - const { completesIngestion } = await persistCallRecordingProgress(client, { - id: callRecording.id, - current: callRecording, - updateData, - }); - - return { - status: 'updated', - event, - callRecordingId: callRecording.id, - callRecordingStatus: completesIngestion - ? CallRecordingStatus.COMPLETED - : (updateData.status ?? callRecordingStatus), - }; -}; - -const findMatchingCallRecording = async ({ - client, - webhookEvent, -}: { - client: CoreApiClient; - webhookEvent: RecallWebhookEvent; -}): Promise => { - if (!isUndefined(webhookEvent.callRecordingIdFromMetadata)) { - return findCallRecordingByFilter(client, { - id: { eq: webhookEvent.callRecordingIdFromMetadata }, - }); - } - - if (isUndefined(webhookEvent.externalBotId)) { - return undefined; - } - - return findCallRecordingByFilter(client, { - externalBotId: { eq: webhookEvent.externalBotId }, - }); -}; - -const findCallRecordingByFilter = async ( - client: CoreApiClient, - filter: Record, -): Promise => { - const queryResult = await client.query({ - callRecordings: { - __args: { - filter, - first: 1, - }, - edges: { - node: { - id: true, - status: true, - startedAt: true, - endedAt: true, - externalRecordingId: true, - transcript: true, - audio: { fileId: true }, - video: { fileId: true }, - }, - }, - }, - }); - - const node = queryResult.callRecordings?.edges?.[0]?.node; - - if (isUndefined(node) || isNull(node)) { - return undefined; - } - - return { - id: node.id, - status: getString(node.status), - startedAt: getString(node.startedAt), - endedAt: getString(node.endedAt), - externalRecordingId: getString(node.externalRecordingId), - transcript: node.transcript ?? undefined, - audio: node.audio ?? undefined, - video: node.video ?? undefined, - }; -}; - -const mapRecallEventToCallRecordingStatus = ({ - event, - statusCode, -}: { - event: string; - statusCode: string | undefined; -}): CallRecordingStatus | undefined => { - if (event === 'recording.done') { - return CallRecordingStatus.PROCESSING; - } - - if (event === 'recording.failed') { - return CallRecordingStatus.FAILED; - } - - return mapRecallStatusCodeToCallRecordingStatus(statusCode); -}; - -const buildRecordingTimestampsUpdate = ({ - webhookEvent, - callRecording, -}: { - webhookEvent: RecallWebhookEvent; - callRecording: MatchedCallRecording; -}): { startedAt?: string; endedAt?: string } => { - const { event, statusCode, statusTimestamp } = webhookEvent; - - const impliesRecordingStarted = statusCode === 'in_call_recording'; - const impliesRecordingEnded = - event === 'recording.done' || - statusCode === 'call_ended' || - statusCode === 'done'; - - const startedAt = - webhookEvent.recordingStartedAt ?? - (impliesRecordingStarted ? statusTimestamp : undefined); - const endedAt = - webhookEvent.recordingEndedAt ?? - (impliesRecordingEnded ? statusTimestamp : undefined); - - return { - ...(!isUndefined(startedAt) && isUndefined(callRecording.startedAt) - ? { startedAt } - : {}), - ...(!isUndefined(endedAt) && isUndefined(callRecording.endedAt) - ? { endedAt } - : {}), - }; -}; - -const buildExternalRecordingIdUpdate = ( - webhookEvent: RecallWebhookEvent, -): { externalRecordingId?: string } => - isUndefined(webhookEvent.externalRecordingId) - ? {} - : { externalRecordingId: webhookEvent.externalRecordingId }; - -type NonFailedCallRecordingStatus = Exclude< - CallRecordingStatus, - CallRecordingStatus.FAILED ->; - -type CallRecordingStatusUpdate = - | { - status: NonFailedCallRecordingStatus; - } - | { - status: CallRecordingStatus.FAILED; - meetingBotFailureReason: string; - }; - -type TerminalArtifactGateFailureUpdate = { - status: CallRecordingStatus.FAILED; - meetingBotFailureReason: string; -}; - -const buildCallRecordingStatusUpdate = ({ - reason, - status, -}: { - reason: string; - status: CallRecordingStatus; -}): CallRecordingStatusUpdate => { - if (status === CallRecordingStatus.FAILED) { - return { status, meetingBotFailureReason: reason }; - } - - return { status }; -}; - -const buildTerminalArtifactGateFailureUpdate = ({ - callRecording, - providerLookupFailed, - updateData, - webhookEvent, -}: { - callRecording: MatchedCallRecording; - providerLookupFailed: boolean; - updateData: CallRecordingUpdateFields; - webhookEvent: RecallWebhookEvent; -}): TerminalArtifactGateFailureUpdate | undefined => { - if (updateData.status === CallRecordingStatus.FAILED) { - return isUndefined(updateData.meetingBotFailureReason) - ? { - status: CallRecordingStatus.FAILED, - meetingBotFailureReason: getRecallWebhookFailureReason(webhookEvent), - } - : undefined; - } - - if ( - providerLookupFailed || - hasRecordingArtifactPath({ callRecording, updateData }) - ) { - return undefined; - } - - return { - status: CallRecordingStatus.FAILED, - meetingBotFailureReason: 'recording_artifacts_unavailable', - }; -}; - -const getRecallWebhookFailureReason = ({ - event, - statusCode, -}: RecallWebhookEvent): string => statusCode ?? event; - -const hasRecordingArtifactPath = ({ - callRecording, - updateData, -}: { - callRecording: MatchedCallRecording; - updateData: CallRecordingUpdateFields; -}): boolean => { - return ( - !isUndefined( - updateData.externalRecordingId ?? callRecording.externalRecordingId, - ) || - isNonEmptyArray(updateData.audio ?? callRecording.audio) || - isNonEmptyArray(updateData.video ?? callRecording.video) || - hasReachableTranscript(updateData.transcript ?? callRecording.transcript) - ); -}; - -const hasReachableTranscript = (transcript: unknown): boolean => { - if (isNull(transcript) || isUndefined(transcript)) { - return false; - } - - const marker = parseTranscriptMarker(transcript); - - return isUndefined(marker) || marker.status === 'PENDING'; -}; - -const isTranscriptUnset = (callRecording: MatchedCallRecording): boolean => - isUndefined(callRecording.transcript); - -const buildMediaIngestionUpdate = async ({ - callRecording, - externalRecordingId, -}: { - callRecording: MatchedCallRecording; - externalRecordingId: string | undefined; -}): Promise> => { - const hasAudio = isNonEmptyArray(callRecording.audio); - const hasVideo = isNonEmptyArray(callRecording.video); - - if (hasAudio && hasVideo) { - return {}; - } - - if (isUndefined(externalRecordingId)) { - console.warn( - `[twenty-meeting-bot] cannot ingest media for call recording ${callRecording.id}: no Recall recording id available`, - ); - - return {}; - } - - return ingestCallRecordingMedia({ - callRecordingId: callRecording.id, - externalRecordingId, - hasAudio, - hasVideo, - }); -}; - -const buildTranscriptArtifactUpdate = async ({ - callRecording, - externalRecordingId, -}: { - callRecording: MatchedCallRecording; - externalRecordingId: string | undefined; -}): Promise => { - if (isUndefined(externalRecordingId)) { - console.warn( - `[twenty-meeting-bot] cannot reconcile transcript for call recording ${callRecording.id}: no Recall recording id available`, - ); - - return {}; - } - - const transcriptArtifactResult = - await reconcileCallRecordingTranscriptArtifact({ - callRecordingId: callRecording.id, - currentStatus: callRecording.status, - externalRecordingId, - requestedAt: new Date().toISOString(), - transcript: callRecording.transcript, - }); - - return { - ...(isUndefined(callRecording.externalRecordingId) - ? { externalRecordingId } - : {}), - ...transcriptArtifactResult.updateData, - }; -}; - -const resolveExternalRecordingId = async ({ - callRecording, - webhookEvent, -}: { - callRecording: MatchedCallRecording; - webhookEvent: RecallWebhookEvent; -}): Promise => { - const externalRecordingId = - webhookEvent.externalRecordingId ?? callRecording.externalRecordingId; - - if (!isUndefined(externalRecordingId)) { - return { externalRecordingId, providerLookupFailed: false }; - } - - if (isUndefined(webhookEvent.externalBotId)) { - return { externalRecordingId: undefined, providerLookupFailed: false }; - } - - return fetchExternalRecordingIdFromRecallBot(webhookEvent.externalBotId); -}; - -const fetchExternalRecordingIdFromRecallBot = async ( - externalBotId: string, -): Promise => { - const botResult = await getRecallBot({ externalBotId }); - - if (!botResult.ok) { - console.warn( - `[twenty-meeting-bot] failed to fetch Recall bot ${externalBotId} while resolving a recording id: ${botResult.errorMessage}`, - ); - - return { externalRecordingId: undefined, providerLookupFailed: true }; - } - - return { - externalRecordingId: extractRecallBotConvergence(botResult.bot) - .externalRecordingId, - providerLookupFailed: false, - }; -}; - -const handleRecallTranscriptEvent = async ({ - client, - webhookEvent, - event, -}: { - client: CoreApiClient; - webhookEvent: RecallWebhookEvent; - event: 'transcript.done' | 'transcript.failed'; -}): Promise => { - const callRecording = await findMatchingCallRecording({ - client, - webhookEvent, - }); - - if (isUndefined(callRecording)) { - return { - status: 'skipped', - event, - reason: 'no matching call recording', - }; - } - - const { transcriptId } = webhookEvent; - - if (event === 'transcript.failed') { - return applyTranscriptFailure({ - client, - callRecording, - event, - transcriptId, - subCode: webhookEvent.transcriptFailureSubCode ?? null, - }); - } - - if (isUndefined(transcriptId)) { - return { - status: 'skipped', - event, - reason: 'missing transcript id', - }; - } - - const downloadResult = await downloadTranscript({ transcriptId }); - - switch (downloadResult.outcome) { - case 'filled': { - const updateData: CallRecordingUpdateFields = { - transcript: downloadResult.content as Record, - ...(isUndefined(callRecording.externalRecordingId) - ? buildExternalRecordingIdUpdate(webhookEvent) - : {}), - }; - - await persistCallRecordingProgress(client, { - id: callRecording.id, - current: callRecording, - updateData, - }); - - return { - status: 'updated', - event, - callRecordingId: callRecording.id, - transcriptOutcome: 'FILLED', - }; - } - case 'failed': - return applyTranscriptFailure({ - client, - callRecording, - event, - transcriptId, - subCode: downloadResult.subCode, - }); - case 'pending': - case 'error': { - // 200-acked either way, Svix never redelivers; the cron re-check retries this. - const reason = - downloadResult.outcome === 'pending' - ? 'transcript not downloadable yet' - : downloadResult.errorMessage; - - console.warn( - `[twenty-meeting-bot] could not fill transcript for call recording ${callRecording.id}: ${reason}`, - ); - - return { - status: 'skipped', - event, - reason, - }; - } - } -}; - -const applyTranscriptFailure = async ({ - client, - callRecording, - event, - transcriptId, - subCode, -}: { - client: CoreApiClient; - callRecording: MatchedCallRecording; - event: string; - transcriptId: string | undefined; - subCode: string | null; -}): Promise => { - const existingMarker = parseTranscriptMarker(callRecording.transcript); - - if (!isTranscriptUnset(callRecording) && isUndefined(existingMarker)) { - return { - status: 'skipped', - event, - reason: 'transcript already filled', - }; - } - - console.warn( - `[twenty-meeting-bot] transcript failed for call recording ${callRecording.id}${isNull(subCode) ? '' : ` (${subCode})`}`, - ); - - await updateCallRecording(client, { - id: callRecording.id, - data: { - transcript: buildFailedTranscriptMarker({ - recallTranscriptId: - transcriptId ?? existingMarker?.recallTranscriptId ?? null, - subCode, - }), - meetingBotFailureReason: buildTranscriptFailureReason(subCode), - ...(isCallRecordingStatusDowngrade({ - fromStatus: callRecording.status, - toStatus: CallRecordingStatus.FAILED, - }) - ? {} - : { status: CallRecordingStatus.FAILED }), - }, - }); - - return { - status: 'updated', - event, - callRecordingId: callRecording.id, - transcriptOutcome: 'FAILED', - }; -}; diff --git a/packages/twenty-apps/public/twenty-meeting-bot/src/logic-functions/flows/heal-call-recordings-missing-bot.util.ts b/packages/twenty-apps/public/twenty-meeting-bot/src/logic-functions/flows/heal-call-recordings-missing-bot.util.ts deleted file mode 100644 index a445a00a2acee..0000000000000 --- a/packages/twenty-apps/public/twenty-meeting-bot/src/logic-functions/flows/heal-call-recordings-missing-bot.util.ts +++ /dev/null @@ -1,82 +0,0 @@ -import { isUndefined } from '@sniptt/guards'; -import { type CoreApiClient } from 'twenty-client-sdk/core'; - -import { type CalendarEventRecord } from 'src/logic-functions/types/calendar-event-record.type'; -import { ensureMeetingBot } from 'src/logic-functions/flows/ensure-meeting-bot.util'; -import { fetchCalendarEventsByIds } from 'src/logic-functions/data/fetch-calendar-events-by-ids.util'; -import { findOpenScheduledCallRecordings } from 'src/logic-functions/data/find-open-scheduled-call-recordings.util'; -import { getUniqueSortedIds } from 'src/logic-functions/utils/get-unique-sorted-ids.util'; - -export type HealCallRecordingsMissingBotResult = { - scheduledCallRecordingIds: string[]; -}; - -// Closes the create-winner crash gap: a run that inserted the row but died before POSTing leaves a botless recording, and the cron is the single writer that re-POSTs it. -export const healCallRecordingsMissingBot = async ({ - client, - now, -}: { - client: CoreApiClient; - now: Date; -}): Promise => { - const botlessCallRecordings = ( - await findOpenScheduledCallRecordings(client) - ).filter((callRecording) => isUndefined(callRecording.externalBotId)); - - if (botlessCallRecordings.length === 0) { - return { scheduledCallRecordingIds: [] }; - } - - const calendarEventsById = new Map( - ( - await fetchCalendarEventsByIds( - client, - getUniqueSortedIds( - botlessCallRecordings.map( - (callRecording) => callRecording.calendarEventId, - ), - ), - ) - ).map((calendarEvent) => [calendarEvent.id, calendarEvent]), - ); - const scheduledCallRecordingIds: string[] = []; - - for (const callRecording of botlessCallRecordings) { - const calendarEvent = isUndefined(callRecording.calendarEventId) - ? undefined - : calendarEventsById.get(callRecording.calendarEventId); - - if (isUndefined(calendarEvent) || hasMeetingEnded({ calendarEvent, now })) { - continue; - } - - const didScheduleMeetingBot = await ensureMeetingBot(client, { - callRecording, - calendarEvent, - }); - - if (didScheduleMeetingBot) { - scheduledCallRecordingIds.push(callRecording.id); - } - } - - return { scheduledCallRecordingIds }; -}; - -const hasMeetingEnded = ({ - calendarEvent, - now, -}: { - calendarEvent: CalendarEventRecord; - now: Date; -}): boolean => { - const reference = calendarEvent.endsAt ?? calendarEvent.startsAt; - - if (isUndefined(reference)) { - return false; - } - - const referenceTime = new Date(reference).getTime(); - - return !Number.isNaN(referenceTime) && referenceTime <= now.getTime(); -}; diff --git a/packages/twenty-apps/public/twenty-meeting-bot/src/logic-functions/flows/ingest-call-recording-media.util.ts b/packages/twenty-apps/public/twenty-meeting-bot/src/logic-functions/flows/ingest-call-recording-media.util.ts deleted file mode 100644 index 79259d9ce26c8..0000000000000 --- a/packages/twenty-apps/public/twenty-meeting-bot/src/logic-functions/flows/ingest-call-recording-media.util.ts +++ /dev/null @@ -1,127 +0,0 @@ -import { isUndefined } from '@sniptt/guards'; -import { MetadataApiClient } from 'twenty-client-sdk/metadata'; - -import { CALL_RECORDING_AUDIO_FIELD_UNIVERSAL_IDENTIFIER } from 'src/constants/call-recording-audio-field-universal-identifier'; -import { CALL_RECORDING_VIDEO_FIELD_UNIVERSAL_IDENTIFIER } from 'src/constants/call-recording-video-field-universal-identifier'; -import { extractRecallMediaUrls } from 'src/logic-functions/recall-api/extract-recall-media-urls.util'; -import { getRecallRecording } from 'src/logic-functions/recall-api/get-recall-recording.util'; -import { type CallRecordingUpdateFields } from 'src/logic-functions/data/update-call-recording.util'; - -type CallRecordingMediaUpdateFields = Pick< - CallRecordingUpdateFields, - 'audio' | 'video' ->; - -const MEDIA_DOWNLOAD_TIMEOUT_MS = 120_000; - -export const ingestCallRecordingMedia = async ({ - callRecordingId, - externalRecordingId, - hasAudio, - hasVideo, -}: { - callRecordingId: string; - externalRecordingId: string; - hasAudio: boolean; - hasVideo: boolean; -}): Promise => { - if (hasAudio && hasVideo) { - return {}; - } - - const recordingResult = await getRecallRecording({ externalRecordingId }); - - if (!recordingResult.ok) { - console.warn( - `[twenty-meeting-bot] failed to fetch Recall recording ${externalRecordingId} while ingesting media for call recording ${callRecordingId}: ${recordingResult.errorMessage}`, - ); - - return {}; - } - - const mediaUrls = extractRecallMediaUrls(recordingResult.recording); - const metadataClient = new MetadataApiClient(); - const updateFields: CallRecordingMediaUpdateFields = {}; - - if (!hasVideo && !isUndefined(mediaUrls.videoUrl)) { - const video = await ingestMediaArtifact({ - callRecordingId, - metadataClient, - url: mediaUrls.videoUrl, - fileName: 'video.mp4', - fieldMetadataUniversalIdentifier: - CALL_RECORDING_VIDEO_FIELD_UNIVERSAL_IDENTIFIER, - }); - - if (!isUndefined(video)) { - updateFields.video = video; - } - } - - if (!hasAudio && !isUndefined(mediaUrls.audioUrl)) { - const audio = await ingestMediaArtifact({ - callRecordingId, - metadataClient, - url: mediaUrls.audioUrl, - fileName: 'audio.mp3', - fieldMetadataUniversalIdentifier: - CALL_RECORDING_AUDIO_FIELD_UNIVERSAL_IDENTIFIER, - }); - - if (!isUndefined(audio)) { - updateFields.audio = audio; - } - } - - return updateFields; -}; - -const ingestMediaArtifact = async ({ - callRecordingId, - metadataClient, - url, - fileName, - fieldMetadataUniversalIdentifier, -}: { - callRecordingId: string; - metadataClient: InstanceType; - url: string; - fileName: string; - fieldMetadataUniversalIdentifier: string; -}): Promise<{ fileId: string; label: string }[] | undefined> => { - try { - const { buffer, contentType } = await downloadMediaFile(url); - const uploadedFile = await metadataClient.uploadFile( - buffer, - fileName, - contentType, - fieldMetadataUniversalIdentifier, - ); - - return [{ fileId: uploadedFile.id, label: fileName }]; - } catch (error) { - console.warn( - `[twenty-meeting-bot] failed to ingest ${fileName} for call recording ${callRecordingId}: ${error instanceof Error ? error.message : String(error)}`, - ); - - return undefined; - } -}; - -const downloadMediaFile = async ( - url: string, -): Promise<{ buffer: Buffer; contentType: string }> => { - const response = await fetch(url, { - signal: AbortSignal.timeout(MEDIA_DOWNLOAD_TIMEOUT_MS), - }); - - if (!response.ok) { - throw new Error(`download failed with status ${response.status}`); - } - - return { - buffer: Buffer.from(await response.arrayBuffer()), - contentType: - response.headers.get('content-type') ?? 'application/octet-stream', - }; -}; diff --git a/packages/twenty-apps/public/twenty-meeting-bot/src/logic-functions/flows/persist-call-recording-progress.util.ts b/packages/twenty-apps/public/twenty-meeting-bot/src/logic-functions/flows/persist-call-recording-progress.util.ts deleted file mode 100644 index d72f9c402cd62..0000000000000 --- a/packages/twenty-apps/public/twenty-meeting-bot/src/logic-functions/flows/persist-call-recording-progress.util.ts +++ /dev/null @@ -1,60 +0,0 @@ -import { type CoreApiClient } from 'twenty-client-sdk/core'; - -import { type FilesFieldValue } from 'src/logic-functions/types/files-field-value.type'; -import { completeAndChargeCallRecording } from 'src/logic-functions/flows/complete-and-charge-call-recording.util'; -import { shouldCompleteCallRecordingIngestion } from 'src/logic-functions/domain/should-complete-call-recording-ingestion.util'; -import { - updateCallRecording, - type CallRecordingUpdateFields, -} from 'src/logic-functions/data/update-call-recording.util'; - -type PersistCallRecordingProgressCurrent = { - status?: string; - startedAt?: string; - endedAt?: string; - transcript?: unknown; - audio?: FilesFieldValue; - video?: FilesFieldValue; -}; - -export const persistCallRecordingProgress = async ( - client: CoreApiClient, - { - id, - current, - updateData, - }: { - id: string; - current: PersistCallRecordingProgressCurrent; - updateData: CallRecordingUpdateFields; - }, -): Promise<{ completesIngestion: boolean }> => { - const completesIngestion = shouldCompleteCallRecordingIngestion({ - current, - updateData, - }); - - if (!completesIngestion) { - await updateCallRecording(client, { id, data: updateData }); - - return { completesIngestion: false }; - } - - // Strip status so COMPLETED is written only by the atomic claim — its single winner bills once. - const nonStatusUpdate: CallRecordingUpdateFields = { ...updateData }; - - delete nonStatusUpdate.status; - delete nonStatusUpdate.meetingBotFailureReason; - - if (Object.keys(nonStatusUpdate).length > 0) { - await updateCallRecording(client, { id, data: nonStatusUpdate }); - } - - await completeAndChargeCallRecording(client, { - id, - startedAt: updateData.startedAt ?? current.startedAt, - endedAt: updateData.endedAt ?? current.endedAt, - }); - - return { completesIngestion: true }; -}; diff --git a/packages/twenty-apps/public/twenty-meeting-bot/src/logic-functions/flows/reap-orphaned-meeting-bots.util.ts b/packages/twenty-apps/public/twenty-meeting-bot/src/logic-functions/flows/reap-orphaned-meeting-bots.util.ts deleted file mode 100644 index f7c8a35cbbdf2..0000000000000 --- a/packages/twenty-apps/public/twenty-meeting-bot/src/logic-functions/flows/reap-orphaned-meeting-bots.util.ts +++ /dev/null @@ -1,185 +0,0 @@ -import { isNull, isUndefined } from '@sniptt/guards'; -import { type CoreApiClient } from 'twenty-client-sdk/core'; - -import { CallRecordingRequestStatus } from 'src/logic-functions/constants/call-recording-request-status'; -import { type CallRecordingRecord } from 'src/logic-functions/types/call-recording-record.type'; -import { cancelRecallBot } from 'src/logic-functions/recall-api/cancel-recall-bot.util'; -import { ejectRecallBot } from 'src/logic-functions/recall-api/eject-recall-bot.util'; -import { findCallRecordingsByIds } from 'src/logic-functions/data/find-call-recordings-by-ids.util'; -import { getCurrentWorkspaceId } from 'src/logic-functions/data/get-current-workspace-id.util'; -import { getUniqueSortedIds } from 'src/logic-functions/utils/get-unique-sorted-ids.util'; -import { isNonEmptyString } from 'src/logic-functions/utils/is-non-empty-string.util'; -import { - listScheduledRecallBots, - type RecallScheduledBot, -} from 'src/logic-functions/recall-api/list-scheduled-recall-bots.util'; - -export type ReapOrphanedMeetingBotsResult = { - scannedBotCount: number; - canceledExternalBotIds: string[]; -}; - -// Bots no open CallRecording request claims would still join; cancel them on Recall. -export const reapOrphanedMeetingBots = async ({ - client, - joinAtAfter, - joinAtBefore, -}: { - client: CoreApiClient; - joinAtAfter: string; - joinAtBefore: string; -}): Promise => { - const listResult = await listScheduledRecallBots({ - joinAtAfter, - joinAtBefore, - }); - - if (!listResult.ok) { - console.warn( - `[twenty-meeting-bot] failed to list Recall bots for orphan reaping: ${listResult.errorMessage}`, - ); - - return { scannedBotCount: 0, canceledExternalBotIds: [] }; - } - - const currentWorkspaceId = getCurrentWorkspaceId(); - - if (isUndefined(currentWorkspaceId)) { - console.warn( - '[twenty-meeting-bot] cannot reap orphaned Recall bots: workspace id unavailable', - ); - - return { - scannedBotCount: listResult.bots.length, - canceledExternalBotIds: [], - }; - } - - const workspaceManagedBots = listResult.bots.filter((bot) => - isCurrentWorkspaceManagedBot({ bot, currentWorkspaceId }), - ); - - if (workspaceManagedBots.length === 0) { - return { - scannedBotCount: listResult.bots.length, - canceledExternalBotIds: [], - }; - } - - const callRecordings = await findCallRecordingsByIds( - client, - getUniqueSortedIds( - workspaceManagedBots.map((bot) => getClaimedCallRecordingId(bot)), - ), - ); - const callRecordingsById = new Map( - callRecordings.map((callRecording) => [callRecording.id, callRecording]), - ); - const canceledExternalBotIds: string[] = []; - - for (const bot of workspaceManagedBots) { - const claimedCallRecordingId = getClaimedCallRecordingId(bot); - const callRecording = isUndefined(claimedCallRecordingId) - ? undefined - : callRecordingsById.get(claimedCallRecordingId); - - if (isBotClaimed({ bot, callRecording })) { - continue; - } - - console.warn( - `[twenty-meeting-bot] canceling orphaned Recall bot ${bot.id} (claimed callRecording: ${claimedCallRecordingId})`, - ); - - if (await cancelOrEjectRecallBot(bot.id)) { - canceledExternalBotIds.push(bot.id); - } - } - - return { - scannedBotCount: listResult.bots.length, - canceledExternalBotIds, - }; -}; - -const getClaimedCallRecordingId = ( - bot: RecallScheduledBot, -): string | undefined => { - const claimedCallRecordingId = bot.metadata.twentyCallRecordingId; - - return normalizeOptionalString(claimedCallRecordingId); -}; - -const getClaimedWorkspaceId = ( - bot: RecallScheduledBot, -): string | undefined => { - const claimedWorkspaceId = bot.metadata.twentyWorkspaceId; - - return normalizeOptionalString(claimedWorkspaceId); -}; - -const isCurrentWorkspaceManagedBot = ({ - bot, - currentWorkspaceId, -}: { - bot: RecallScheduledBot; - currentWorkspaceId: string; -}): boolean => { - if (isUndefined(getClaimedCallRecordingId(bot))) { - return false; - } - - const claimedWorkspaceId = getClaimedWorkspaceId(bot); - - return claimedWorkspaceId === currentWorkspaceId; -}; - -const isBotClaimed = ({ - bot, - callRecording, -}: { - bot: RecallScheduledBot; - callRecording: CallRecordingRecord | undefined; -}): boolean => { - if ( - callRecording?.recordingRequestStatus !== - CallRecordingRequestStatus.REQUESTED - ) { - return false; - } - - if (callRecording.externalBotId === bot.id) { - return true; - } - - // An id-less REQUESTED recording may have a bot-id write-back in flight; spare its bot. - return isUndefined(callRecording.externalBotId); -}; - -const cancelOrEjectRecallBot = async ( - externalBotId: string, -): Promise => { - const cancelResult = await cancelRecallBot({ externalBotId }); - - if (cancelResult.ok) { - return true; - } - - // Deleting only works for not-yet-joined bots; eject the ones already in a call. - if (!isNull(cancelResult.status)) { - const ejectResult = await ejectRecallBot({ externalBotId }); - - if (ejectResult.ok) { - return true; - } - } - - console.warn( - `[twenty-meeting-bot] failed to cancel orphaned Recall bot ${externalBotId}: ${cancelResult.errorMessage}`, - ); - - return false; -}; - -const normalizeOptionalString = (value: unknown): string | undefined => - isNonEmptyString(value) ? value.trim() : undefined; diff --git a/packages/twenty-apps/public/twenty-meeting-bot/src/logic-functions/flows/reconcile-call-recording-transcript-artifact-result.type.ts b/packages/twenty-apps/public/twenty-meeting-bot/src/logic-functions/flows/reconcile-call-recording-transcript-artifact-result.type.ts deleted file mode 100644 index d7226cd24353a..0000000000000 --- a/packages/twenty-apps/public/twenty-meeting-bot/src/logic-functions/flows/reconcile-call-recording-transcript-artifact-result.type.ts +++ /dev/null @@ -1,11 +0,0 @@ -import { type CallRecordingUpdateFields } from 'src/logic-functions/data/update-call-recording.util'; - -type CallRecordingTranscriptArtifactUpdateFields = Pick< - CallRecordingUpdateFields, - 'meetingBotFailureReason' | 'status' | 'transcript' ->; - -export type ReconcileCallRecordingTranscriptArtifactResult = { - updateData: CallRecordingTranscriptArtifactUpdateFields; - requestedTranscript: boolean; -}; diff --git a/packages/twenty-apps/public/twenty-meeting-bot/src/logic-functions/flows/reconcile-call-recording-transcript-artifact.util.ts b/packages/twenty-apps/public/twenty-meeting-bot/src/logic-functions/flows/reconcile-call-recording-transcript-artifact.util.ts deleted file mode 100644 index a657ae961a20f..0000000000000 --- a/packages/twenty-apps/public/twenty-meeting-bot/src/logic-functions/flows/reconcile-call-recording-transcript-artifact.util.ts +++ /dev/null @@ -1,183 +0,0 @@ -import { isNull, isUndefined } from '@sniptt/guards'; - -import { CallRecordingStatus } from 'src/logic-functions/constants/call-recording-status'; -import { buildFailedTranscriptMarker } from 'src/logic-functions/domain/build-failed-transcript-marker.util'; -import { buildPendingTranscriptMarker } from 'src/logic-functions/domain/build-pending-transcript-marker.util'; -import { buildTranscriptFailureReason } from 'src/logic-functions/domain/build-transcript-failure-reason.util'; -import { isCallRecordingStatusDowngrade } from 'src/logic-functions/domain/is-call-recording-status-downgrade.util'; -import { parseTranscriptMarker } from 'src/logic-functions/domain/parse-transcript-marker.util'; -import { createAsyncRecallTranscript } from 'src/logic-functions/recall-api/create-async-recall-transcript.util'; -import { listRecallTranscripts } from 'src/logic-functions/recall-api/list-recall-transcripts.util'; -import { type RecallTranscriptSummary } from 'src/logic-functions/recall-api/recall-transcript-summary.type'; -import { downloadTranscript } from 'src/logic-functions/flows/download-transcript.util'; -import { type ReconcileCallRecordingTranscriptArtifactResult } from 'src/logic-functions/flows/reconcile-call-recording-transcript-artifact-result.type'; - -type CallRecordingTranscriptArtifactUpdateFields = - ReconcileCallRecordingTranscriptArtifactResult['updateData']; - -export const reconcileCallRecordingTranscriptArtifact = async ({ - callRecordingId, - currentStatus, - externalRecordingId, - requestedAt, - transcript, -}: { - callRecordingId: string; - currentStatus: string | undefined; - externalRecordingId: string; - requestedAt: string; - transcript: unknown; -}): Promise => { - const existingTranscriptMarker = parseTranscriptMarker(transcript); - - if ( - !isNull(transcript) && - !isUndefined(transcript) && - isUndefined(existingTranscriptMarker) - ) { - return buildEmptyTranscriptArtifactResult(); - } - - if (existingTranscriptMarker?.status === 'FAILED') { - return buildEmptyTranscriptArtifactResult(); - } - - const listResult = await listRecallTranscripts({ externalRecordingId }); - - if (!listResult.ok) { - console.warn( - `[twenty-meeting-bot] failed to list Recall transcripts for recording ${externalRecordingId}: ${listResult.errorMessage}`, - ); - - return buildEmptyTranscriptArtifactResult(); - } - - const transcriptArtifact = selectRecallTranscriptArtifact( - listResult.transcripts, - ); - const pendingTranscriptMarkerRecallTranscriptId = - existingTranscriptMarker?.status === 'PENDING' - ? (existingTranscriptMarker.recallTranscriptId ?? undefined) - : undefined; - const transcriptIdToDownload = - transcriptArtifact?.id ?? pendingTranscriptMarkerRecallTranscriptId; - - if ( - isUndefined(transcriptArtifact) && - isUndefined(pendingTranscriptMarkerRecallTranscriptId) - ) { - const createResult = await createAsyncRecallTranscript({ - externalRecordingId, - callRecordingId, - }); - - if (!createResult.ok) { - console.warn( - `[twenty-meeting-bot] failed to request transcript for Recall recording ${externalRecordingId}: ${createResult.errorMessage}`, - ); - - return buildEmptyTranscriptArtifactResult(); - } - - return { - updateData: { - transcript: buildPendingTranscriptMarker({ - recallTranscriptId: createResult.transcriptId, - requestedAt, - }), - }, - requestedTranscript: true, - }; - } - - if ( - !isUndefined(transcriptArtifact) && - (transcriptArtifact.statusCode === 'failed' || - transcriptArtifact.statusCode === 'error') - ) { - return { - updateData: buildTranscriptFailureUpdate({ - currentStatus, - transcriptId: transcriptArtifact.id, - subCode: transcriptArtifact.statusSubCode ?? null, - }), - requestedTranscript: false, - }; - } - - if ( - !isUndefined(transcriptArtifact) && - transcriptArtifact.statusCode !== 'done' - ) { - return buildEmptyTranscriptArtifactResult(); - } - - if (isUndefined(transcriptIdToDownload)) { - return buildEmptyTranscriptArtifactResult(); - } - - const downloadResult = await downloadTranscript({ - transcriptId: transcriptIdToDownload, - }); - - if (downloadResult.outcome === 'filled') { - return { - updateData: { - transcript: downloadResult.content as Record, - }, - requestedTranscript: false, - }; - } - - if (downloadResult.outcome === 'failed') { - return { - updateData: buildTranscriptFailureUpdate({ - currentStatus, - transcriptId: transcriptIdToDownload, - subCode: downloadResult.subCode, - }), - requestedTranscript: false, - }; - } - - if (downloadResult.outcome === 'error') { - console.warn( - `[twenty-meeting-bot] could not fill transcript for call recording ${callRecordingId}: ${downloadResult.errorMessage}`, - ); - } - - return buildEmptyTranscriptArtifactResult(); -}; - -const buildEmptyTranscriptArtifactResult = - (): ReconcileCallRecordingTranscriptArtifactResult => ({ - updateData: {}, - requestedTranscript: false, - }); - -const selectRecallTranscriptArtifact = ( - transcripts: RecallTranscriptSummary[], -): RecallTranscriptSummary | undefined => - transcripts.find((transcript) => transcript.statusCode !== 'deleted'); - -const buildTranscriptFailureUpdate = ({ - currentStatus, - transcriptId, - subCode, -}: { - currentStatus: string | undefined; - transcriptId: string; - subCode: string | null; -}): CallRecordingTranscriptArtifactUpdateFields => ({ - transcript: buildFailedTranscriptMarker({ - recallTranscriptId: transcriptId, - subCode, - }), - meetingBotFailureReason: buildTranscriptFailureReason(subCode), - ...(isCallRecordingStatusDowngrade({ - fromStatus: currentStatus, - toStatus: CallRecordingStatus.FAILED, - }) - ? {} - : { status: CallRecordingStatus.FAILED }), -}); diff --git a/packages/twenty-apps/public/twenty-meeting-bot/src/logic-functions/flows/reconcile-meeting-bot.util.ts b/packages/twenty-apps/public/twenty-meeting-bot/src/logic-functions/flows/reconcile-meeting-bot.util.ts deleted file mode 100644 index 052e3b96299f5..0000000000000 --- a/packages/twenty-apps/public/twenty-meeting-bot/src/logic-functions/flows/reconcile-meeting-bot.util.ts +++ /dev/null @@ -1,496 +0,0 @@ -import { isUndefined } from '@sniptt/guards'; -import { type CoreApiClient } from 'twenty-client-sdk/core'; - -import { CallRecordingRequestStatus } from 'src/logic-functions/constants/call-recording-request-status'; -import { CallRecordingStatus } from 'src/logic-functions/constants/call-recording-status'; -import { type CalendarEventRecord } from 'src/logic-functions/types/calendar-event-record.type'; -import { type CallRecordingRecord } from 'src/logic-functions/types/call-recording-record.type'; -import { type MeetingBotPolicyResultForMeeting } from 'src/logic-functions/types/meeting-bot-policy-result-for-meeting.type'; -import { type MeetingBotReconciliationResult } from 'src/logic-functions/types/meeting-bot-reconciliation-result.type'; -import { type RemovedMeetingBotOccurrence } from 'src/logic-functions/types/removed-meeting-bot-occurrence.type'; -import { aggregateMeetingBotPolicyResultsByMeeting } from 'src/logic-functions/domain/aggregate-meeting-bot-policy-results-by-meeting.util'; -import { buildMeetingBotPolicyResult } from 'src/logic-functions/domain/build-meeting-bot-policy-result.util'; -import { cancelCallRecordingRequest } from 'src/logic-functions/flows/cancel-call-recording-request.util'; -import { computeCallRecordingIdForMeeting } from 'src/logic-functions/domain/compute-call-recording-id-for-meeting.util'; -import { - createCallRecording, - type ScheduledCallRecordingFields, -} from 'src/logic-functions/data/create-call-recording.util'; -import { ensureMeetingBot } from 'src/logic-functions/flows/ensure-meeting-bot.util'; -import { fetchCalendarEventsByIds } from 'src/logic-functions/data/fetch-calendar-events-by-ids.util'; -import { fetchCalendarEventsByStartsAtValues } from 'src/logic-functions/data/fetch-calendar-events-by-starts-at-values.util'; -import { findCallRecordingsByCalendarEventIds } from 'src/logic-functions/data/find-call-recordings-by-calendar-event-ids.util'; -import { findCallRecordingsByIds } from 'src/logic-functions/data/find-call-recordings-by-ids.util'; -import { getUniqueSortedIds } from 'src/logic-functions/utils/get-unique-sorted-ids.util'; -import { rescheduleCallRecordingBot } from 'src/logic-functions/flows/reschedule-call-recording-bot.util'; -import { - updateCallRecording, - type CallRecordingUpdateFields, -} from 'src/logic-functions/data/update-call-recording.util'; - -export const reconcileMeetingBotForCalendarEventIds = async ({ - client, - calendarEventIds, - removedOccurrences = [], - now = new Date(), -}: { - client: CoreApiClient; - calendarEventIds: string[]; - removedOccurrences?: RemovedMeetingBotOccurrence[]; - now?: Date; -}): Promise => { - const meetingPolicyResults = await resolveMeetingBotPolicyResultsForMeetings({ - client, - calendarEventIds, - removedOccurrences, - now, - }); - - return reconcileMeetingBotForMeetingOccurrences({ - client, - meetingPolicyResults, - removedOccurrences, - }); -}; - -const resolveMeetingBotPolicyResultsForMeetings = async ({ - client, - calendarEventIds, - removedOccurrences = [], - now = new Date(), -}: { - client: CoreApiClient; - calendarEventIds: string[]; - removedOccurrences?: RemovedMeetingBotOccurrence[]; - now?: Date; -}): Promise => { - const changedCalendarEvents = await fetchCalendarEventsByIds( - client, - getUniqueSortedIds(calendarEventIds), - ); - const affectedMeetingKeys = new Set(); - const occurrenceStartsAtAnchors = new Set(); - const changedCalendarEventPolicyResults = changedCalendarEvents.map( - (calendarEvent) => buildMeetingBotPolicyResult(calendarEvent, now), - ); - - for (const policyResult of changedCalendarEventPolicyResults) { - affectedMeetingKeys.add(policyResult.realMeetingKey); - } - - for (const calendarEvent of changedCalendarEvents) { - if (!isUndefined(calendarEvent.startsAt)) { - occurrenceStartsAtAnchors.add(calendarEvent.startsAt); - } - } - - for (const removedOccurrence of removedOccurrences) { - affectedMeetingKeys.add(removedOccurrence.realMeetingKey); - - if (!isUndefined(removedOccurrence.startsAt)) { - occurrenceStartsAtAnchors.add(removedOccurrence.startsAt); - } - } - - if (affectedMeetingKeys.size === 0) { - return []; - } - - const occurrenceSiblingEvents = await fetchCalendarEventsByStartsAtValues( - client, - [...occurrenceStartsAtAnchors], - ); - const policyResultsByCalendarEventId = new Map( - changedCalendarEventPolicyResults.map((policyResult) => [ - policyResult.calendarEventId, - policyResult, - ]), - ); - - for (const calendarEvent of occurrenceSiblingEvents) { - if (policyResultsByCalendarEventId.has(calendarEvent.id)) { - continue; - } - - policyResultsByCalendarEventId.set( - calendarEvent.id, - buildMeetingBotPolicyResult(calendarEvent, now), - ); - } - - const perCalendarEventPolicyResults = [ - ...policyResultsByCalendarEventId.values(), - ] - .filter((policyResult) => - affectedMeetingKeys.has(policyResult.realMeetingKey), - ) - .map((policyResult) => ({ - calendarEventId: policyResult.calendarEventId, - realMeetingKey: policyResult.realMeetingKey, - shouldRequestBot: policyResult.shouldRequestBot, - })); - const meetingPolicyResults = aggregateMeetingBotPolicyResultsByMeeting( - perCalendarEventPolicyResults, - ); - const meetingKeysWithPolicyResult = new Set( - meetingPolicyResults.map( - (meetingPolicyResult) => meetingPolicyResult.realMeetingKey, - ), - ); - - for (const meetingKey of [...affectedMeetingKeys].sort()) { - if (meetingKeysWithPolicyResult.has(meetingKey)) { - continue; - } - - meetingPolicyResults.push({ - realMeetingKey: meetingKey, - shouldRequestBot: false, - calendarEventIds: [], - requestingCalendarEventIds: [], - }); - } - - return meetingPolicyResults; -}; - -const reconcileMeetingBotForMeetingOccurrences = async ({ - client, - meetingPolicyResults, - removedOccurrences = [], -}: { - client: CoreApiClient; - meetingPolicyResults: MeetingBotPolicyResultForMeeting[]; - removedOccurrences?: RemovedMeetingBotOccurrence[]; -}): Promise => { - const removedCalendarEventIdsByMeetingKey = - buildRemovedCalendarEventIdsByMeetingKey(removedOccurrences); - const reconciliationResults: MeetingBotReconciliationResult[] = []; - const orderedMeetingPolicyResults = [ - ...meetingPolicyResults.filter( - (meetingPolicyResult) => !meetingPolicyResult.shouldRequestBot, - ), - ...meetingPolicyResults.filter( - (meetingPolicyResult) => meetingPolicyResult.shouldRequestBot, - ), - ]; - - for (const meetingPolicyResult of orderedMeetingPolicyResults) { - const removedCalendarEventIds = - removedCalendarEventIdsByMeetingKey.get( - meetingPolicyResult.realMeetingKey, - ) ?? []; - - try { - reconciliationResults.push( - meetingPolicyResult.shouldRequestBot - ? await reconcileActiveMeeting({ - client, - meetingPolicyResult, - removedCalendarEventIds, - }) - : await reconcileCanceledMeeting({ - client, - meetingPolicyResult, - removedCalendarEventIds, - }), - ); - } catch (error) { - const errorMessage = - error instanceof Error ? error.message : String(error); - - console.error( - `[twenty-meeting-bot] reconciliation failed for meeting ${meetingPolicyResult.realMeetingKey}: ${errorMessage}`, - ); - reconciliationResults.push({ - action: 'FAILED', - realMeetingKey: meetingPolicyResult.realMeetingKey, - errorMessage, - }); - } - } - - return reconciliationResults; -}; - -const reconcileActiveMeeting = async ({ - client, - meetingPolicyResult, - removedCalendarEventIds, -}: { - client: CoreApiClient; - meetingPolicyResult: MeetingBotPolicyResultForMeeting; - removedCalendarEventIds: string[]; -}): Promise => { - const representativeCalendarEventId = getUniqueSortedIds( - meetingPolicyResult.requestingCalendarEventIds, - )[0]; - - if (isUndefined(representativeCalendarEventId)) { - return buildSkippedResult(meetingPolicyResult.realMeetingKey); - } - - const representativeCalendarEvent = ( - await fetchCalendarEventsByIds(client, [representativeCalendarEventId]) - )[0]; - - if (isUndefined(representativeCalendarEvent)) { - return buildSkippedResult(meetingPolicyResult.realMeetingKey); - } - - const callRecordingId = computeCallRecordingIdForMeeting( - meetingPolicyResult.realMeetingKey, - ); - const existingCallRecording = ( - await findCallRecordingsByIds(client, [callRecordingId]) - )[0]; - - if (!isUndefined(existingCallRecording)) { - return updatePolicyManagedCallRecording({ - client, - existingCallRecording, - representativeCalendarEvent, - realMeetingKey: meetingPolicyResult.realMeetingKey, - }); - } - - const manualOpenCallRecording = await findManualOpenCallRecording({ - client, - meetingPolicyResult, - removedCalendarEventIds, - }); - - if (!isUndefined(manualOpenCallRecording)) { - return { - action: 'SKIPPED', - realMeetingKey: meetingPolicyResult.realMeetingKey, - callRecordingId: manualOpenCallRecording.id, - }; - } - - return createPolicyManagedCallRecording({ - client, - callRecordingId, - representativeCalendarEvent, - realMeetingKey: meetingPolicyResult.realMeetingKey, - }); -}; - -const updatePolicyManagedCallRecording = async ({ - client, - existingCallRecording, - representativeCalendarEvent, - realMeetingKey, -}: { - client: CoreApiClient; - existingCallRecording: CallRecordingRecord; - representativeCalendarEvent: CalendarEventRecord; - realMeetingKey: string; -}): Promise => { - await updateCallRecording(client, { - id: existingCallRecording.id, - data: buildPolicyManagedCallRecordingUpdateFields({ - existingCallRecording, - calendarEvent: representativeCalendarEvent, - }), - }); - await rescheduleCallRecordingBot(client, { - callRecording: existingCallRecording, - calendarEvent: representativeCalendarEvent, - }); - - return { - action: 'UPDATED', - realMeetingKey, - callRecordingId: existingCallRecording.id, - }; -}; - -const createPolicyManagedCallRecording = async ({ - client, - callRecordingId, - representativeCalendarEvent, - realMeetingKey, -}: { - client: CoreApiClient; - callRecordingId: string; - representativeCalendarEvent: CalendarEventRecord; - realMeetingKey: string; -}): Promise => { - const scheduledFields = buildScheduledCallRecordingFields( - representativeCalendarEvent, - ); - - try { - await createCallRecording(client, { - id: callRecordingId, - data: scheduledFields, - }); - } catch (error) { - // The id is deterministic, so a conflict means a concurrent run created the row first. - const concurrentlyCreatedCallRecording = ( - await findCallRecordingsByIds(client, [callRecordingId]) - )[0]; - - if (isUndefined(concurrentlyCreatedCallRecording)) { - throw error; - } - - return updatePolicyManagedCallRecording({ - client, - existingCallRecording: concurrentlyCreatedCallRecording, - representativeCalendarEvent, - realMeetingKey, - }); - } - - // Winning the deterministic-id insert elects this run as the single writer that creates the bot. - await ensureMeetingBot(client, { - callRecording: { - id: callRecordingId, - ...scheduledFields, - title: scheduledFields.title ?? undefined, - }, - calendarEvent: representativeCalendarEvent, - }); - - return { - action: 'CREATED', - realMeetingKey, - callRecordingId, - }; -}; - -const findManualOpenCallRecording = async ({ - client, - meetingPolicyResult, - removedCalendarEventIds, -}: { - client: CoreApiClient; - meetingPolicyResult: MeetingBotPolicyResultForMeeting; - removedCalendarEventIds: string[]; -}): Promise => { - const calendarEventIds = getUniqueSortedIds([ - ...meetingPolicyResult.calendarEventIds, - ...meetingPolicyResult.requestingCalendarEventIds, - ...removedCalendarEventIds, - ]); - const callRecordings = await findCallRecordingsByCalendarEventIds( - client, - calendarEventIds, - ); - - return [...callRecordings] - .sort((firstCallRecording, secondCallRecording) => - firstCallRecording.id.localeCompare(secondCallRecording.id), - ) - .find( - (callRecording) => - callRecording.status !== CallRecordingStatus.COMPLETED && - isUndefined(callRecording.recordingRequestStatus), - ); -}; - -const reconcileCanceledMeeting = async ({ - client, - meetingPolicyResult, - removedCalendarEventIds, -}: { - client: CoreApiClient; - meetingPolicyResult: MeetingBotPolicyResultForMeeting; - removedCalendarEventIds: string[]; -}): Promise => { - const calendarEventIds = getUniqueSortedIds([ - ...meetingPolicyResult.calendarEventIds, - ...removedCalendarEventIds, - ]); - const cancellableCallRecordings = ( - await findCallRecordingsByCalendarEventIds(client, calendarEventIds) - ).filter( - (callRecording) => - callRecording.status === CallRecordingStatus.SCHEDULED && - callRecording.recordingRequestStatus === - CallRecordingRequestStatus.REQUESTED, - ); - - if (cancellableCallRecordings.length === 0) { - return buildSkippedResult(meetingPolicyResult.realMeetingKey); - } - - for (const callRecording of cancellableCallRecordings) { - await cancelCallRecordingRequest({ - client, - callRecording, - }); - } - - return { - action: 'CANCELED', - realMeetingKey: meetingPolicyResult.realMeetingKey, - callRecordingId: cancellableCallRecordings[0].id, - }; -}; - -// startedAt/endedAt come from the webhook; calendar writes never touch them. -const buildCalendarDrivenCallRecordingFields = ( - calendarEvent: CalendarEventRecord, -): Omit => ({ - // Wire null clears a stale title when the calendar title is gone or restricted. - title: calendarEvent.title ?? null, - recordingRequestStatus: CallRecordingRequestStatus.REQUESTED, - calendarEventId: calendarEvent.id, -}); - -const buildScheduledCallRecordingFields = ( - calendarEvent: CalendarEventRecord, -): ScheduledCallRecordingFields => ({ - ...buildCalendarDrivenCallRecordingFields(calendarEvent), - status: CallRecordingStatus.SCHEDULED, -}); - -// A live or finished bot lifecycle must never be reset to SCHEDULED by a calendar-driven update. -const buildPolicyManagedCallRecordingUpdateFields = ({ - existingCallRecording, - calendarEvent, -}: { - existingCallRecording: CallRecordingRecord; - calendarEvent: CalendarEventRecord; -}): CallRecordingUpdateFields => - canResetCallRecordingStatusToScheduled(existingCallRecording.status) - ? { - ...buildScheduledCallRecordingFields(calendarEvent), - ...(isUndefined(existingCallRecording.meetingBotFailureReason) - ? {} - : { meetingBotFailureReason: null }), - } - : buildCalendarDrivenCallRecordingFields(calendarEvent); - -const canResetCallRecordingStatusToScheduled = ( - status: string | undefined, -): boolean => - status === CallRecordingStatus.SCHEDULED || - status === CallRecordingStatus.FAILED; - -const buildRemovedCalendarEventIdsByMeetingKey = ( - removedOccurrences: RemovedMeetingBotOccurrence[], -): Map => { - const calendarEventIdsByMeetingKey = new Map(); - - for (const removedOccurrence of removedOccurrences) { - calendarEventIdsByMeetingKey.set(removedOccurrence.realMeetingKey, [ - ...(calendarEventIdsByMeetingKey.get(removedOccurrence.realMeetingKey) ?? - []), - removedOccurrence.calendarEventId, - ]); - } - - return calendarEventIdsByMeetingKey; -}; - -const buildSkippedResult = ( - realMeetingKey: string, -): MeetingBotReconciliationResult => ({ - action: 'SKIPPED', - realMeetingKey, - callRecordingId: null, -}); diff --git a/packages/twenty-apps/public/twenty-meeting-bot/src/logic-functions/flows/reschedule-call-recording-bot.util.ts b/packages/twenty-apps/public/twenty-meeting-bot/src/logic-functions/flows/reschedule-call-recording-bot.util.ts deleted file mode 100644 index f3be32c694a6a..0000000000000 --- a/packages/twenty-apps/public/twenty-meeting-bot/src/logic-functions/flows/reschedule-call-recording-bot.util.ts +++ /dev/null @@ -1,70 +0,0 @@ -import { isUndefined } from '@sniptt/guards'; -import { type CoreApiClient } from 'twenty-client-sdk/core'; - -import { type MeetingRecording } from 'src/logic-functions/types/meeting-recording.type'; -import { buildRecallBotMetadata } from 'src/logic-functions/domain/build-recall-bot-metadata.util'; -import { computeRecallBotJoinAt } from 'src/logic-functions/domain/compute-recall-bot-join-at.util'; -import { getCurrentWorkspaceId } from 'src/logic-functions/data/get-current-workspace-id.util'; -import { rescheduleRecallBot } from 'src/logic-functions/recall-api/reschedule-recall-bot.util'; -import { updateCallRecording } from 'src/logic-functions/data/update-call-recording.util'; - -const RECALL_BOT_NOT_FOUND_STATUS = 404; - -export const rescheduleCallRecordingBot = async ( - client: CoreApiClient, - { callRecording, calendarEvent }: MeetingRecording, -): Promise => { - const externalBotId = callRecording.externalBotId; - - if (isUndefined(externalBotId)) { - return; - } - - const meetingUrl = calendarEvent.conferenceLinkUrl; - const meetingStartsAt = calendarEvent.startsAt; - - if (isUndefined(meetingUrl) || isUndefined(meetingStartsAt)) { - return; - } - - const joinAt = computeRecallBotJoinAt(meetingStartsAt); - - const workspaceId = getCurrentWorkspaceId(); - - if (isUndefined(workspaceId)) { - console.warn( - `[twenty-meeting-bot] cannot reschedule Recall bot for callRecording ${callRecording.id}: workspace id unavailable`, - ); - - return; - } - - const rescheduleResult = await rescheduleRecallBot({ - externalBotId, - meetingUrl, - joinAt, - metadata: buildRecallBotMetadata({ - callRecording, - calendarEvent, - workspaceId, - }), - }); - - if (rescheduleResult.ok) { - return; - } - - // The bot vanished externally; drop the id so the stale-state cron re-creates it as the single writer. - if (rescheduleResult.status === RECALL_BOT_NOT_FOUND_STATUS) { - await updateCallRecording(client, { - id: callRecording.id, - data: { externalBotId: null }, - }); - - return; - } - - console.warn( - `[twenty-meeting-bot] failed to update Recall bot for callRecording ${callRecording.id}: ${rescheduleResult.errorMessage}`, - ); -}; diff --git a/packages/twenty-apps/public/twenty-meeting-bot/src/logic-functions/recall-api/__tests__/extract-recall-bot-convergence.test.ts b/packages/twenty-apps/public/twenty-meeting-bot/src/logic-functions/recall-api/__tests__/extract-recall-bot-convergence.test.ts deleted file mode 100644 index 0b2ece8240ccc..0000000000000 --- a/packages/twenty-apps/public/twenty-meeting-bot/src/logic-functions/recall-api/__tests__/extract-recall-bot-convergence.test.ts +++ /dev/null @@ -1,153 +0,0 @@ -import { describe, expect, it } from 'vitest'; - -import { extractRecallBotConvergence } from 'src/logic-functions/recall-api/extract-recall-bot-convergence.util'; - -describe('extractRecallBotConvergence', () => { - it('maps the latest status change code to a call recording status', () => { - const convergence = extractRecallBotConvergence({ - status_changes: [ - { code: 'joining_call', created_at: '2026-01-01T12:58:00.000Z' }, - { code: 'in_call_recording', created_at: '2026-01-01T13:02:00.000Z' }, - { code: 'call_ended', created_at: '2026-01-01T14:00:00.000Z' }, - { code: 'done', created_at: '2026-01-01T14:05:00.000Z' }, - ], - }); - - // COMPLETED is reserved for full artifact ingestion, never bot state. - expect(convergence.status).toBe('PROCESSING'); - expect(convergence.isRecallRecordingDone).toBe(true); - }); - - it('uses created_at to find the latest status when Recall returns status changes out of order', () => { - const convergence = extractRecallBotConvergence({ - status_changes: [ - { code: 'done', created_at: '2026-01-01T14:05:00.000Z' }, - { code: 'joining_call', created_at: '2026-01-01T12:58:00.000Z' }, - { code: 'in_call_recording', created_at: '2026-01-01T13:02:00.000Z' }, - ], - }); - - expect(convergence.status).toBe('PROCESSING'); - }); - - it('prefers recording-object timestamps over status change entries', () => { - const convergence = extractRecallBotConvergence({ - status_changes: [ - { code: 'in_call_recording', created_at: '2026-01-01T13:02:30.000Z' }, - { code: 'call_ended', created_at: '2026-01-01T14:00:30.000Z' }, - ], - recordings: [ - { - id: 'recall-recording-1', - started_at: '2026-01-01T13:02:00.000Z', - completed_at: '2026-01-01T14:00:00.000Z', - }, - ], - }); - - expect(convergence).toEqual({ - status: 'PROCESSING', - failureReason: undefined, - startedAt: '2026-01-01T13:02:00.000Z', - endedAt: '2026-01-01T14:00:00.000Z', - externalRecordingId: 'recall-recording-1', - isRecallRecordingDone: true, - }); - }); - - it('falls back to status change timestamps when recordings carry none', () => { - const convergence = extractRecallBotConvergence({ - status_changes: [ - { code: 'in_call_recording', created_at: '2026-01-01T13:02:00.000Z' }, - { code: 'call_ended', created_at: '2026-01-01T14:00:00.000Z' }, - ], - recordings: [{ id: 'recall-recording-1' }], - }); - - expect(convergence).toEqual({ - status: 'PROCESSING', - failureReason: undefined, - startedAt: '2026-01-01T13:02:00.000Z', - endedAt: '2026-01-01T14:00:00.000Z', - externalRecordingId: 'recall-recording-1', - isRecallRecordingDone: false, - }); - }); - - it('normalizes microsecond-precision Recall timestamps to millisecond ISO', () => { - const convergence = extractRecallBotConvergence({ - status_changes: [ - { code: 'done', created_at: '2026-06-10T12:20:00.123456+00:00' }, - ], - recordings: [ - { - id: 'recall-recording-1', - started_at: '2026-06-10T11:02:28.281597+00:00', - completed_at: '2026-06-10T12:17:28.281597+00:00', - }, - ], - }); - - expect(convergence.startedAt).toBe('2026-06-10T11:02:28.281Z'); - expect(convergence.endedAt).toBe('2026-06-10T12:17:28.281Z'); - }); - - it('returns nothing derivable from an empty bot response', () => { - expect(extractRecallBotConvergence({})).toEqual({ - status: undefined, - failureReason: undefined, - startedAt: undefined, - endedAt: undefined, - externalRecordingId: undefined, - isRecallRecordingDone: false, - }); - }); - - it('skips malformed status change entries', () => { - const convergence = extractRecallBotConvergence({ - status_changes: [ - null, - 'not-an-object', - { created_at: '2026-01-01T13:00:00.000Z' }, - { code: 'in_call_recording', created_at: '2026-01-01T13:02:00.000Z' }, - ], - recordings: 'not-an-array', - }); - - expect(convergence).toEqual({ - status: 'RECORDING', - failureReason: undefined, - startedAt: '2026-01-01T13:02:00.000Z', - endedAt: undefined, - externalRecordingId: undefined, - isRecallRecordingDone: false, - }); - }); - - it('carries the failing Recall status code as the failure reason', () => { - const convergence = extractRecallBotConvergence({ - status_changes: [ - { code: 'joining_call', created_at: '2026-01-01T12:58:00.000Z' }, - { - code: 'recording_permission_denied', - created_at: '2026-01-01T13:02:00.000Z', - }, - ], - }); - - expect(convergence.status).toBe('FAILED'); - expect(convergence.failureReason).toBe('recording_permission_denied'); - }); - - it('leaves the status undefined for unknown latest codes', () => { - const convergence = extractRecallBotConvergence({ - status_changes: [ - { code: 'in_call_recording', created_at: '2026-01-01T13:02:00.000Z' }, - { code: 'some_future_code', created_at: '2026-01-01T13:30:00.000Z' }, - ], - }); - - expect(convergence.status).toBeUndefined(); - expect(convergence.startedAt).toBe('2026-01-01T13:02:00.000Z'); - }); -}); diff --git a/packages/twenty-apps/public/twenty-meeting-bot/src/logic-functions/recall-api/__tests__/extract-recall-media-urls.test.ts b/packages/twenty-apps/public/twenty-meeting-bot/src/logic-functions/recall-api/__tests__/extract-recall-media-urls.test.ts deleted file mode 100644 index 1f254c3cc9d86..0000000000000 --- a/packages/twenty-apps/public/twenty-meeting-bot/src/logic-functions/recall-api/__tests__/extract-recall-media-urls.test.ts +++ /dev/null @@ -1,67 +0,0 @@ -import { describe, expect, it } from 'vitest'; - -import { extractRecallMediaUrls } from 'src/logic-functions/recall-api/extract-recall-media-urls.util'; - -describe('extractRecallMediaUrls', () => { - it('reads both download urls flat from the v1.11 media shortcuts', () => { - expect( - extractRecallMediaUrls({ - id: 'recall-recording-1', - media_shortcuts: { - video_mixed: { - download_url: 'https://media.example.com/video.mp4', - }, - audio_mixed: { - download_url: 'https://media.example.com/audio.mp3', - }, - }, - }), - ).toEqual({ - videoUrl: 'https://media.example.com/video.mp4', - audioUrl: 'https://media.example.com/audio.mp3', - }); - }); - - it('falls back to the nested data.download_url shape', () => { - expect( - extractRecallMediaUrls({ - id: 'recall-recording-1', - media_shortcuts: { - video_mixed: { - data: { download_url: 'https://media.example.com/video.mp4' }, - }, - audio_mixed: { - data: { download_url: 'https://media.example.com/audio.mp3' }, - }, - }, - }), - ).toEqual({ - videoUrl: 'https://media.example.com/video.mp4', - audioUrl: 'https://media.example.com/audio.mp3', - }); - }); - - it('returns undefined urls when artifacts are absent', () => { - expect( - extractRecallMediaUrls({ - id: 'recall-recording-1', - media_shortcuts: { - video_mixed: {}, - }, - }), - ).toEqual({ videoUrl: undefined, audioUrl: undefined }); - }); - - it('tolerates malformed recording payloads', () => { - expect(extractRecallMediaUrls({})).toEqual({ - videoUrl: undefined, - audioUrl: undefined, - }); - expect(extractRecallMediaUrls({ media_shortcuts: 'not-a-record' })).toEqual( - { - videoUrl: undefined, - audioUrl: undefined, - }, - ); - }); -}); diff --git a/packages/twenty-apps/public/twenty-meeting-bot/src/logic-functions/recall-api/__tests__/recall-bot-api.test.ts b/packages/twenty-apps/public/twenty-meeting-bot/src/logic-functions/recall-api/__tests__/recall-bot-api.test.ts deleted file mode 100644 index 8e301c55d9b49..0000000000000 --- a/packages/twenty-apps/public/twenty-meeting-bot/src/logic-functions/recall-api/__tests__/recall-bot-api.test.ts +++ /dev/null @@ -1,795 +0,0 @@ -import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; - -import { cancelRecallBot } from 'src/logic-functions/recall-api/cancel-recall-bot.util'; -import { createAsyncRecallTranscript } from 'src/logic-functions/recall-api/create-async-recall-transcript.util'; -import { ejectRecallBot } from 'src/logic-functions/recall-api/eject-recall-bot.util'; -import { getRecallBot } from 'src/logic-functions/recall-api/get-recall-bot.util'; -import { listRecallTranscripts } from 'src/logic-functions/recall-api/list-recall-transcripts.util'; -import { listScheduledRecallBots } from 'src/logic-functions/recall-api/list-scheduled-recall-bots.util'; -import { rescheduleRecallBot } from 'src/logic-functions/recall-api/reschedule-recall-bot.util'; -import { retrieveRecallTranscript } from 'src/logic-functions/recall-api/retrieve-recall-transcript.util'; -import { scheduleRecallBot } from 'src/logic-functions/recall-api/schedule-recall-bot.util'; -import { MEETING_BOT_RECORDING_RETENTION_HOURS_ENV_VAR_NAME } from 'src/logic-functions/constants/meeting-bot-recording-retention-hours-env-var-name'; - -const getRecallApiConfigMock = vi.hoisted(() => vi.fn()); -const WORKSPACE_ID = '123e4567-e89b-12d3-a456-426614174000'; - -vi.mock('src/logic-functions/recall-api/get-recall-api-config.util', () => ({ - getRecallApiConfig: getRecallApiConfigMock, -})); - -describe('recall bot api', () => { - const fetchMock = vi.fn(); - - beforeEach(() => { - delete process.env[MEETING_BOT_RECORDING_RETENTION_HOURS_ENV_VAR_NAME]; - getRecallApiConfigMock.mockReset(); - getRecallApiConfigMock.mockReturnValue({ - success: true, - config: { - apiKey: 'recall-api-key', - baseUrl: 'https://ap-northeast-1.recall.ai/api/v1', - botName: 'Twenty Meeting Bot', - }, - }); - fetchMock.mockReset(); - fetchMock.mockResolvedValue({ - ok: true, - status: 201, - json: async () => ({ id: 'recall-bot-id' }), - }); - vi.stubGlobal('fetch', fetchMock); - }); - - it('creates Recall bot requests with the Token authorization scheme', async () => { - const result = await scheduleRecallBot({ - meetingUrl: 'https://meet.google.com/abc-defg-hij', - joinAt: '2026-01-01T13:00:00.000Z', - metadata: { - twentyWorkspaceId: WORKSPACE_ID, - twentyCallRecordingId: 'call-recording-id', - twentyCalendarEventId: 'calendar-event-id', - twentyRealMeetingKey: 'meeting-key', - }, - }); - - expect(result).toEqual({ ok: true, externalBotId: 'recall-bot-id' }); - expect(fetchMock).toHaveBeenCalledWith( - 'https://ap-northeast-1.recall.ai/api/v1/bot/', - expect.objectContaining({ - method: 'POST', - headers: expect.objectContaining({ - Authorization: 'Token recall-api-key', - 'Content-Type': 'application/json', - }), - }), - ); - expect(JSON.parse(fetchMock.mock.calls[0][1].body)).toEqual({ - meeting_url: 'https://meet.google.com/abc-defg-hij', - join_at: '2026-01-01T13:00:00.000Z', - bot_name: 'Twenty Meeting Bot', - recording_config: { - video_mixed_mp4: {}, - audio_mixed_mp3: {}, - retention: { type: 'timed', hours: 166 }, - }, - metadata: { - twentyWorkspaceId: WORKSPACE_ID, - twentyCallRecordingId: 'call-recording-id', - twentyCalendarEventId: 'calendar-event-id', - twentyRealMeetingKey: 'meeting-key', - }, - }); - }); - - it('uses the configured Recall recording retention hours when scheduling a bot', async () => { - process.env[MEETING_BOT_RECORDING_RETENTION_HOURS_ENV_VAR_NAME] = '240'; - - const result = await scheduleRecallBot({ - meetingUrl: 'https://meet.google.com/abc-defg-hij', - joinAt: '2026-01-01T13:00:00.000Z', - metadata: { - twentyWorkspaceId: WORKSPACE_ID, - twentyCallRecordingId: 'call-recording-id', - twentyCalendarEventId: 'calendar-event-id', - twentyRealMeetingKey: 'meeting-key', - }, - }); - - expect(result).toEqual({ ok: true, externalBotId: 'recall-bot-id' }); - expect(JSON.parse(fetchMock.mock.calls[0][1].body).recording_config).toEqual( - { - video_mixed_mp4: {}, - audio_mixed_mp3: {}, - retention: { type: 'timed', hours: 240 }, - }, - ); - }); - - it('falls back to safe Recall recording retention hours when the configured value is invalid', async () => { - process.env[MEETING_BOT_RECORDING_RETENTION_HOURS_ENV_VAR_NAME] = - 'seven-days'; - - const result = await scheduleRecallBot({ - meetingUrl: 'https://meet.google.com/abc-defg-hij', - joinAt: '2026-01-01T13:00:00.000Z', - metadata: { - twentyWorkspaceId: WORKSPACE_ID, - twentyCallRecordingId: 'call-recording-id', - twentyCalendarEventId: 'calendar-event-id', - twentyRealMeetingKey: 'meeting-key', - }, - }); - - expect(result).toEqual({ ok: true, externalBotId: 'recall-bot-id' }); - expect(JSON.parse(fetchMock.mock.calls[0][1].body).recording_config).toEqual( - { - video_mixed_mp4: {}, - audio_mixed_mp3: {}, - retention: { type: 'timed', hours: 166 }, - }, - ); - }); - - it('fails when the create response does not include a bot id', async () => { - fetchMock.mockResolvedValue({ - ok: true, - status: 201, - json: async () => ({}), - }); - - const result = await scheduleRecallBot({ - meetingUrl: 'https://meet.google.com/abc-defg-hij', - joinAt: '2026-01-01T13:00:00.000Z', - metadata: { - twentyWorkspaceId: WORKSPACE_ID, - twentyCallRecordingId: 'call-recording-id', - twentyCalendarEventId: 'calendar-event-id', - twentyRealMeetingKey: 'meeting-key', - }, - }); - - expect(result).toEqual({ - ok: false, - status: null, - errorMessage: - 'Recall API created a bot but the response did not include a bot id', - }); - }); - - it('reports the HTTP status when rescheduling a bot that no longer exists', async () => { - fetchMock.mockResolvedValue({ - ok: false, - status: 404, - json: async () => ({ detail: 'Not found.' }), - }); - - const result = await rescheduleRecallBot({ - externalBotId: 'recall-bot-gone', - meetingUrl: 'https://meet.google.com/abc-defg-hij', - joinAt: '2026-01-01T13:00:00.000Z', - metadata: { - twentyWorkspaceId: WORKSPACE_ID, - twentyCallRecordingId: 'call-recording-id', - twentyCalendarEventId: 'calendar-event-id', - twentyRealMeetingKey: 'meeting-key', - }, - }); - - expect(result).toEqual({ - ok: false, - status: 404, - errorMessage: - 'Recall API responded with HTTP 404: {"detail":"Not found."}', - }); - expect(JSON.parse(fetchMock.mock.calls[0][1].body).recording_config).toEqual( - { - video_mixed_mp4: {}, - audio_mixed_mp3: {}, - retention: { type: 'timed', hours: 166 }, - }, - ); - }); - - it('does not duplicate an existing Token authorization prefix', async () => { - getRecallApiConfigMock.mockReturnValue({ - success: true, - config: { - apiKey: 'Token recall-api-key', - baseUrl: 'https://ap-northeast-1.recall.ai/api/v1', - botName: 'Twenty Meeting Bot', - }, - }); - - await scheduleRecallBot({ - meetingUrl: 'https://meet.google.com/abc-defg-hij', - joinAt: '2026-01-01T13:00:00.000Z', - metadata: { - twentyWorkspaceId: WORKSPACE_ID, - twentyCallRecordingId: 'call-recording-id', - twentyCalendarEventId: 'calendar-event-id', - twentyRealMeetingKey: 'meeting-key', - }, - }); - - expect(fetchMock).toHaveBeenCalledWith( - expect.any(String), - expect.objectContaining({ - headers: expect.objectContaining({ - Authorization: 'Token recall-api-key', - }), - }), - ); - }); - - it('lists scheduled bots in a join-at window and follows pagination', async () => { - fetchMock - .mockResolvedValueOnce({ - ok: true, - status: 200, - json: async () => ({ - next: 'https://ap-northeast-1.recall.ai/api/v1/bot/?cursor=page-2', - results: [ - { id: 'bot-1', metadata: { twentyCallRecordingId: 'recording-1' } }, - ], - }), - }) - .mockResolvedValueOnce({ - ok: true, - status: 200, - json: async () => ({ - next: null, - results: [{ id: 'bot-2' }], - }), - }); - - const result = await listScheduledRecallBots({ - joinAtAfter: '2026-01-01T08:00:00.000Z', - joinAtBefore: '2026-01-02T12:00:00.000Z', - }); - - expect(result).toEqual({ - ok: true, - bots: [ - { id: 'bot-1', metadata: { twentyCallRecordingId: 'recording-1' } }, - { id: 'bot-2', metadata: {} }, - ], - }); - expect(fetchMock).toHaveBeenNthCalledWith( - 1, - 'https://ap-northeast-1.recall.ai/api/v1/bot/?join_at_after=2026-01-01T08%3A00%3A00.000Z&join_at_before=2026-01-02T12%3A00%3A00.000Z', - expect.objectContaining({ method: 'GET' }), - ); - expect(fetchMock).toHaveBeenNthCalledWith( - 2, - 'https://ap-northeast-1.recall.ai/api/v1/bot/?cursor=page-2', - expect.objectContaining({ method: 'GET' }), - ); - }); - - it('fails the scheduled bot list when the pagination cap would truncate results', async () => { - for (let pageIndex = 1; pageIndex <= 10; pageIndex++) { - fetchMock.mockResolvedValueOnce({ - ok: true, - status: 200, - json: async () => ({ - next: `https://ap-northeast-1.recall.ai/api/v1/bot/?cursor=page-${pageIndex + 1}`, - results: [{ id: `bot-${pageIndex}` }], - }), - }); - } - - const result = await listScheduledRecallBots({ - joinAtAfter: '2026-01-01T08:00:00.000Z', - joinAtBefore: '2026-01-02T12:00:00.000Z', - }); - - expect(result).toEqual({ - ok: false, - status: null, - errorMessage: 'Recall bot list exceeded 10 pages', - }); - expect(fetchMock).toHaveBeenCalledTimes(10); - }); - - it('stops paginating when the next link points outside the configured region', async () => { - fetchMock.mockResolvedValue({ - ok: true, - status: 200, - json: async () => ({ - next: 'https://evil.example.com/api/v1/bot/?cursor=page-2', - results: [{ id: 'bot-1' }], - }), - }); - - const result = await listScheduledRecallBots({ - joinAtAfter: '2026-01-01T08:00:00.000Z', - joinAtBefore: '2026-01-02T12:00:00.000Z', - }); - - expect(result).toEqual({ - ok: true, - bots: [{ id: 'bot-1', metadata: {} }], - }); - expect(fetchMock).toHaveBeenCalledTimes(1); - }); - - it('cancels a scheduled Recall bot request', async () => { - fetchMock.mockResolvedValue({ - ok: true, - status: 204, - json: async () => ({}), - }); - - const result = await cancelRecallBot({ - externalBotId: 'recall-bot-id', - }); - - expect(result).toEqual({ ok: true }); - expect(fetchMock).toHaveBeenCalledWith( - 'https://ap-northeast-1.recall.ai/api/v1/bot/recall-bot-id/', - expect.objectContaining({ method: 'DELETE' }), - ); - }); - - it('ejects a bot through the leave_call endpoint', async () => { - fetchMock.mockResolvedValue({ - ok: true, - status: 200, - json: async () => ({ id: 'recall-bot-id' }), - }); - - const result = await ejectRecallBot({ - externalBotId: 'recall-bot-id', - }); - - expect(result).toEqual({ ok: true }); - expect(fetchMock).toHaveBeenCalledWith( - 'https://ap-northeast-1.recall.ai/api/v1/bot/recall-bot-id/leave_call/', - expect.objectContaining({ method: 'POST' }), - ); - }); - - it('fetches a single bot and returns the raw response', async () => { - const botResponse = { - id: 'recall-bot-id', - status_changes: [{ code: 'done' }], - recordings: [{ id: 'recall-recording-id' }], - }; - - fetchMock.mockResolvedValue({ - ok: true, - status: 200, - json: async () => botResponse, - }); - - const result = await getRecallBot({ externalBotId: 'recall-bot-id' }); - - expect(result).toEqual({ ok: true, bot: botResponse }); - expect(fetchMock).toHaveBeenCalledWith( - 'https://ap-northeast-1.recall.ai/api/v1/bot/recall-bot-id/', - expect.objectContaining({ method: 'GET' }), - ); - }); - - it('reports the HTTP status when fetching a bot that no longer exists', async () => { - fetchMock.mockResolvedValue({ - ok: false, - status: 404, - json: async () => ({ detail: 'Not found.' }), - }); - - const result = await getRecallBot({ externalBotId: 'recall-bot-gone' }); - - expect(result).toEqual({ - ok: false, - status: 404, - errorMessage: - 'Recall API responded with HTTP 404: {"detail":"Not found."}', - }); - }); - - it('lists transcripts for a recording id and normalizes status fields', async () => { - fetchMock.mockResolvedValue({ - ok: true, - status: 200, - json: async () => ({ - next: null, - results: [ - { - id: 'recall-transcript-id', - status: { code: 'done', sub_code: null }, - }, - ], - }), - }); - - const result = await listRecallTranscripts({ - externalRecordingId: 'recall-recording-id', - }); - - expect(result).toEqual({ - ok: true, - transcripts: [ - { - id: 'recall-transcript-id', - statusCode: 'done', - statusSubCode: undefined, - }, - ], - }); - expect(fetchMock).toHaveBeenCalledWith( - 'https://ap-northeast-1.recall.ai/api/v1/transcript/?recording_id=recall-recording-id', - expect.objectContaining({ method: 'GET' }), - ); - }); - - it('follows transcript list pagination within the configured Recall region', async () => { - fetchMock - .mockResolvedValueOnce({ - ok: true, - status: 200, - json: async () => ({ - next: 'https://ap-northeast-1.recall.ai/api/v1/transcript/?cursor=page-2', - results: [ - { - id: 'recall-transcript-id-1', - status: { code: 'processing' }, - }, - ], - }), - }) - .mockResolvedValueOnce({ - ok: true, - status: 200, - json: async () => ({ - next: null, - results: [ - { - id: 'recall-transcript-id-2', - status: { code: 'failed', sub_code: 'audio_missing' }, - }, - ], - }), - }); - - const result = await listRecallTranscripts({ - externalRecordingId: 'recall-recording-id', - }); - - expect(result).toEqual({ - ok: true, - transcripts: [ - { - id: 'recall-transcript-id-1', - statusCode: 'processing', - statusSubCode: undefined, - }, - { - id: 'recall-transcript-id-2', - statusCode: 'failed', - statusSubCode: 'audio_missing', - }, - ], - }); - expect(fetchMock).toHaveBeenNthCalledWith( - 2, - 'https://ap-northeast-1.recall.ai/api/v1/transcript/?cursor=page-2', - expect.objectContaining({ method: 'GET' }), - ); - }); - - it('rejects malformed transcript lists', async () => { - fetchMock.mockResolvedValue({ - ok: true, - status: 200, - json: async () => ({ - next: null, - results: [{}], - }), - }); - - const result = await listRecallTranscripts({ - externalRecordingId: 'recall-recording-id', - }); - - expect(result).toEqual({ - ok: false, - status: 200, - errorMessage: 'Recall API returned malformed transcript list', - }); - }); - - it('creates an async transcript with the locked provider settings', async () => { - fetchMock.mockResolvedValue({ - ok: true, - status: 201, - json: async () => ({ id: 'recall-transcript-id' }), - }); - - const result = await createAsyncRecallTranscript({ - externalRecordingId: 'recall-recording-id', - }); - - expect(result).toEqual({ ok: true, transcriptId: 'recall-transcript-id' }); - expect(fetchMock).toHaveBeenCalledWith( - 'https://ap-northeast-1.recall.ai/api/v1/recording/recall-recording-id/create_transcript/', - expect.objectContaining({ method: 'POST' }), - ); - expect(JSON.parse(fetchMock.mock.calls[0][1].body)).toEqual({ - provider: { recallai_async: { language_code: 'auto' } }, - diarization: { use_separate_streams_when_available: true }, - }); - }); - - it('adds call recording metadata when convergence creates an async transcript', async () => { - fetchMock.mockResolvedValue({ - ok: true, - status: 201, - json: async () => ({ id: 'recall-transcript-id' }), - }); - - const result = await createAsyncRecallTranscript({ - externalRecordingId: 'recall-recording-id', - callRecordingId: 'call-recording-id', - }); - - expect(result).toEqual({ ok: true, transcriptId: 'recall-transcript-id' }); - expect(JSON.parse(fetchMock.mock.calls[0][1].body)).toEqual({ - provider: { recallai_async: { language_code: 'auto' } }, - diarization: { use_separate_streams_when_available: true }, - metadata: { twentyCallRecordingId: 'call-recording-id' }, - }); - }); - - it('does not retry async transcript creation failures', async () => { - fetchMock.mockResolvedValue({ - ok: false, - status: 503, - json: async () => ({ detail: 'service unavailable' }), - }); - - const result = await createAsyncRecallTranscript({ - externalRecordingId: 'recall-recording-id', - }); - - expect(result).toEqual({ - ok: false, - status: 503, - errorMessage: - 'Recall API responded with HTTP 503: {"detail":"service unavailable"}', - }); - expect(fetchMock).toHaveBeenCalledTimes(1); - }); - - it('fails when the transcript creation response has no id', async () => { - fetchMock.mockResolvedValue({ - ok: true, - status: 201, - json: async () => ({}), - }); - - const result = await createAsyncRecallTranscript({ - externalRecordingId: 'recall-recording-id', - }); - - expect(result).toEqual({ - ok: false, - status: null, - errorMessage: - 'Recall API created a transcript but the response did not include a transcript id', - }); - }); - - it('retrieves transcript details with the download URL and status', async () => { - fetchMock.mockResolvedValue({ - ok: true, - status: 200, - json: async () => ({ - id: 'recall-transcript-id', - status: { code: 'done', sub_code: null }, - data: { - download_url: 'https://recall-transcripts.example.com/transcript', - }, - }), - }); - - const result = await retrieveRecallTranscript({ - transcriptId: 'recall-transcript-id', - }); - - expect(result).toEqual({ - ok: true, - transcript: { - downloadUrl: 'https://recall-transcripts.example.com/transcript', - statusCode: 'done', - statusSubCode: undefined, - }, - }); - expect(fetchMock).toHaveBeenCalledWith( - 'https://ap-northeast-1.recall.ai/api/v1/transcript/recall-transcript-id/', - expect.objectContaining({ method: 'GET' }), - ); - }); - - it('surfaces the failure sub code of an errored transcript', async () => { - fetchMock.mockResolvedValue({ - ok: true, - status: 200, - json: async () => ({ - id: 'recall-transcript-id', - status: { code: 'error', sub_code: 'audio_missing' }, - data: {}, - }), - }); - - const result = await retrieveRecallTranscript({ - transcriptId: 'recall-transcript-id', - }); - - expect(result).toEqual({ - ok: true, - transcript: { - downloadUrl: undefined, - statusCode: 'error', - statusSubCode: 'audio_missing', - }, - }); - }); - - it('rejects malformed transcript details', async () => { - fetchMock - .mockResolvedValueOnce({ - ok: true, - status: 200, - json: async () => ({ - id: 'recall-transcript-id', - status: { code: 'done' }, - data: {}, - }), - }) - .mockResolvedValueOnce({ - ok: true, - status: 200, - json: async () => ({ - id: 'recall-transcript-id', - data: {}, - }), - }); - - await expect( - retrieveRecallTranscript({ transcriptId: 'recall-transcript-id' }), - ).resolves.toEqual({ - ok: false, - status: 200, - errorMessage: 'Recall API returned malformed transcript details', - }); - await expect( - retrieveRecallTranscript({ transcriptId: 'recall-transcript-id' }), - ).resolves.toEqual({ - ok: false, - status: 200, - errorMessage: 'Recall API returned malformed transcript details', - }); - }); - - describe('transient failure retries', () => { - beforeEach(() => { - vi.useFakeTimers(); - }); - - afterEach(() => { - vi.useRealTimers(); - }); - - it('retries a network failure and succeeds on the next attempt', async () => { - fetchMock.mockRejectedValueOnce(new Error('socket hang up')); - fetchMock.mockResolvedValueOnce({ - ok: true, - status: 200, - json: async () => ({ id: 'recall-bot-id' }), - }); - - const resultPromise = getRecallBot({ externalBotId: 'recall-bot-id' }); - - await vi.runAllTimersAsync(); - - expect(await resultPromise).toEqual({ - ok: true, - bot: { id: 'recall-bot-id' }, - }); - expect(fetchMock).toHaveBeenCalledTimes(2); - }); - - it('retries a 503 response and succeeds on the next attempt', async () => { - fetchMock.mockResolvedValueOnce({ - ok: false, - status: 503, - json: async () => ({ detail: 'service unavailable' }), - }); - fetchMock.mockResolvedValueOnce({ - ok: true, - status: 200, - json: async () => ({ id: 'recall-bot-id' }), - }); - - const resultPromise = getRecallBot({ externalBotId: 'recall-bot-id' }); - - await vi.runAllTimersAsync(); - - expect(await resultPromise).toEqual({ - ok: true, - bot: { id: 'recall-bot-id' }, - }); - expect(fetchMock).toHaveBeenCalledTimes(2); - }); - - it('gives up after the attempt budget on persistent server errors', async () => { - fetchMock.mockResolvedValue({ - ok: false, - status: 500, - json: async () => ({ detail: 'server error' }), - }); - - const resultPromise = getRecallBot({ externalBotId: 'recall-bot-id' }); - - await vi.runAllTimersAsync(); - - expect(await resultPromise).toEqual({ - ok: false, - status: 500, - errorMessage: - 'Recall API responded with HTTP 500: {"detail":"server error"}', - }); - expect(fetchMock).toHaveBeenCalledTimes(3); - }); - - it('does not retry client errors', async () => { - fetchMock.mockResolvedValue({ - ok: false, - status: 400, - json: async () => ({ detail: 'bad request' }), - }); - - const result = await getRecallBot({ externalBotId: 'recall-bot-id' }); - - expect(result).toEqual({ - ok: false, - status: 400, - errorMessage: - 'Recall API responded with HTTP 400: {"detail":"bad request"}', - }); - expect(fetchMock).toHaveBeenCalledTimes(1); - }); - - it('does not retry an allowed 404 on cancel', async () => { - fetchMock.mockResolvedValue({ - ok: false, - status: 404, - json: async () => ({ detail: 'not found' }), - }); - - const result = await cancelRecallBot({ - externalBotId: 'recall-bot-id', - }); - - expect(result).toEqual({ ok: true }); - expect(fetchMock).toHaveBeenCalledTimes(1); - }); - - it('does not retry an allowed 404 on eject', async () => { - fetchMock.mockResolvedValue({ - ok: false, - status: 404, - json: async () => ({ detail: 'not found' }), - }); - - const result = await ejectRecallBot({ - externalBotId: 'recall-bot-id', - }); - - expect(result).toEqual({ ok: true }); - expect(fetchMock).toHaveBeenCalledTimes(1); - }); - }); -}); diff --git a/packages/twenty-apps/public/twenty-meeting-bot/src/logic-functions/recall-api/__tests__/verify-recall-webhook-signature.test.ts b/packages/twenty-apps/public/twenty-meeting-bot/src/logic-functions/recall-api/__tests__/verify-recall-webhook-signature.test.ts deleted file mode 100644 index b70c0fb2cd241..0000000000000 --- a/packages/twenty-apps/public/twenty-meeting-bot/src/logic-functions/recall-api/__tests__/verify-recall-webhook-signature.test.ts +++ /dev/null @@ -1,122 +0,0 @@ -import { createHmac } from 'crypto'; - -import { describe, expect, it } from 'vitest'; - -import { verifyRecallWebhookSignature } from 'src/logic-functions/recall-api/verify-recall-webhook-signature.util'; - -const SECRET_BYTES = Buffer.from('test-secret-abc123'); -const SECRET = `whsec_${SECRET_BYTES.toString('base64')}`; -const WEBHOOK_ID = 'msg_123'; -const WEBHOOK_TIMESTAMP = '1760000000'; -const NOW = new Date(Number(WEBHOOK_TIMESTAMP) * 1000); - -const sign = (body: string): string => - createHmac('sha256', SECRET_BYTES) - .update(`${WEBHOOK_ID}.${WEBHOOK_TIMESTAMP}.${body}`) - .digest('base64'); - -describe('verifyRecallWebhookSignature', () => { - it('accepts valid Recall webhook-* signature headers', () => { - const body = JSON.stringify({ event: 'recording.done' }); - - const result = verifyRecallWebhookSignature({ - rawBody: body, - secret: SECRET, - now: NOW, - headers: { - 'webhook-id': WEBHOOK_ID, - 'webhook-timestamp': WEBHOOK_TIMESTAMP, - 'webhook-signature': `v1,${sign(body)}`, - }, - }); - - expect(result).toEqual({ valid: true }); - }); - - it('accepts valid svix-* signature headers', () => { - const body = JSON.stringify({ event: 'recording.done' }); - - const result = verifyRecallWebhookSignature({ - rawBody: body, - secret: SECRET, - now: NOW, - headers: { - 'svix-id': WEBHOOK_ID, - 'svix-timestamp': WEBHOOK_TIMESTAMP, - 'svix-signature': `v1,${sign(body)}`, - }, - }); - - expect(result).toEqual({ valid: true }); - }); - - it('rejects deliveries whose timestamp is outside of the tolerance', () => { - const body = JSON.stringify({ event: 'recording.done' }); - - const result = verifyRecallWebhookSignature({ - rawBody: body, - secret: SECRET, - now: new Date(Number(WEBHOOK_TIMESTAMP) * 1000 + 6 * 60 * 1000), - headers: { - 'webhook-id': WEBHOOK_ID, - 'webhook-timestamp': WEBHOOK_TIMESTAMP, - 'webhook-signature': `v1,${sign(body)}`, - }, - }); - - expect(result).toEqual({ - valid: false, - error: 'Webhook timestamp is outside of the allowed tolerance', - }); - }); - - it('rejects non-numeric timestamps', () => { - const body = JSON.stringify({ event: 'recording.done' }); - - const result = verifyRecallWebhookSignature({ - rawBody: body, - secret: SECRET, - now: NOW, - headers: { - 'webhook-id': WEBHOOK_ID, - 'webhook-timestamp': 'not-a-timestamp', - 'webhook-signature': `v1,${sign(body)}`, - }, - }); - - expect(result).toEqual({ - valid: false, - error: 'Invalid webhook timestamp', - }); - }); - - it('rejects missing signature headers', () => { - const result = verifyRecallWebhookSignature({ - rawBody: '{}', - secret: SECRET, - headers: {}, - }); - - expect(result).toEqual({ - valid: false, - error: 'Missing webhook signature headers', - }); - }); - - it('rejects signatures computed from a different body', () => { - const body = JSON.stringify({ event: 'recording.done' }); - - const result = verifyRecallWebhookSignature({ - rawBody: JSON.stringify({ event: 'recording.failed' }), - secret: SECRET, - now: NOW, - headers: { - 'webhook-id': WEBHOOK_ID, - 'webhook-timestamp': WEBHOOK_TIMESTAMP, - 'webhook-signature': `v1,${sign(body)}`, - }, - }); - - expect(result.valid).toBe(false); - }); -}); diff --git a/packages/twenty-apps/public/twenty-meeting-bot/src/logic-functions/recall-api/cancel-recall-bot.util.ts b/packages/twenty-apps/public/twenty-meeting-bot/src/logic-functions/recall-api/cancel-recall-bot.util.ts deleted file mode 100644 index 04d7bf5ee014e..0000000000000 --- a/packages/twenty-apps/public/twenty-meeting-bot/src/logic-functions/recall-api/cancel-recall-bot.util.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { type RecallBotRemovalResult } from 'src/logic-functions/types/recall-bot-operation-result.type'; -import { getRecallApiConfig } from 'src/logic-functions/recall-api/get-recall-api-config.util'; -import { recallBotApiRequest } from 'src/logic-functions/recall-api/recall-bot-api-request.util'; - -export const cancelRecallBot = async ({ - externalBotId, -}: { - externalBotId: string; -}): Promise => { - const configResult = getRecallApiConfig(); - - if (!configResult.success) { - return { ok: false, status: null, errorMessage: configResult.error }; - } - - const result = await recallBotApiRequest({ - config: configResult.config, - path: `/bot/${externalBotId}/`, - method: 'DELETE', - allowNotFound: true, - }); - - if (!result.ok) { - return result; - } - - return { ok: true }; -}; diff --git a/packages/twenty-apps/public/twenty-meeting-bot/src/logic-functions/recall-api/create-async-recall-transcript.util.ts b/packages/twenty-apps/public/twenty-meeting-bot/src/logic-functions/recall-api/create-async-recall-transcript.util.ts deleted file mode 100644 index c52ed514ee741..0000000000000 --- a/packages/twenty-apps/public/twenty-meeting-bot/src/logic-functions/recall-api/create-async-recall-transcript.util.ts +++ /dev/null @@ -1,52 +0,0 @@ -import { isString } from '@sniptt/guards'; - -import { type RecallBotOperationFailure } from 'src/logic-functions/types/recall-bot-operation-result.type'; -import { getRecallApiConfig } from 'src/logic-functions/recall-api/get-recall-api-config.util'; -import { recallBotApiRequest } from 'src/logic-functions/recall-api/recall-bot-api-request.util'; - -type CreateAsyncRecallTranscriptResult = - | { ok: true; transcriptId: string } - | RecallBotOperationFailure; - -export const createAsyncRecallTranscript = async ({ - externalRecordingId, - callRecordingId, -}: { - externalRecordingId: string; - callRecordingId?: string; -}): Promise => { - const configResult = getRecallApiConfig(); - - if (!configResult.success) { - return { ok: false, status: null, errorMessage: configResult.error }; - } - - const result = await recallBotApiRequest<{ id?: unknown }>({ - config: configResult.config, - path: `/recording/${externalRecordingId}/create_transcript/`, - method: 'POST', - body: { - provider: { recallai_async: { language_code: 'auto' } }, - diarization: { use_separate_streams_when_available: true }, - ...(callRecordingId === undefined - ? {} - : { metadata: { twentyCallRecordingId: callRecordingId } }), - }, - maxAttempts: 1, - }); - - if (!result.ok) { - return result; - } - - if (!isString(result.data?.id)) { - return { - ok: false, - status: null, - errorMessage: - 'Recall API created a transcript but the response did not include a transcript id', - }; - } - - return { ok: true, transcriptId: result.data.id }; -}; diff --git a/packages/twenty-apps/public/twenty-meeting-bot/src/logic-functions/recall-api/eject-recall-bot.util.ts b/packages/twenty-apps/public/twenty-meeting-bot/src/logic-functions/recall-api/eject-recall-bot.util.ts deleted file mode 100644 index c1bd3fc5dc102..0000000000000 --- a/packages/twenty-apps/public/twenty-meeting-bot/src/logic-functions/recall-api/eject-recall-bot.util.ts +++ /dev/null @@ -1,28 +0,0 @@ -import { type RecallBotRemovalResult } from 'src/logic-functions/types/recall-bot-operation-result.type'; -import { getRecallApiConfig } from 'src/logic-functions/recall-api/get-recall-api-config.util'; -import { recallBotApiRequest } from 'src/logic-functions/recall-api/recall-bot-api-request.util'; - -export const ejectRecallBot = async ({ - externalBotId, -}: { - externalBotId: string; -}): Promise => { - const configResult = getRecallApiConfig(); - - if (!configResult.success) { - return { ok: false, status: null, errorMessage: configResult.error }; - } - - const result = await recallBotApiRequest({ - config: configResult.config, - path: `/bot/${externalBotId}/leave_call/`, - method: 'POST', - allowNotFound: true, - }); - - if (!result.ok) { - return result; - } - - return { ok: true }; -}; diff --git a/packages/twenty-apps/public/twenty-meeting-bot/src/logic-functions/recall-api/extract-recall-bot-convergence.util.ts b/packages/twenty-apps/public/twenty-meeting-bot/src/logic-functions/recall-api/extract-recall-bot-convergence.util.ts deleted file mode 100644 index d6af6a3a160bb..0000000000000 --- a/packages/twenty-apps/public/twenty-meeting-bot/src/logic-functions/recall-api/extract-recall-bot-convergence.util.ts +++ /dev/null @@ -1,149 +0,0 @@ -import { isArray, isUndefined } from '@sniptt/guards'; - -import { CallRecordingStatus } from 'src/logic-functions/constants/call-recording-status'; -import { asRecord } from 'src/logic-functions/utils/as-record.util'; -import { getString } from 'src/logic-functions/utils/get-string.util'; -import { mapRecallStatusCodeToCallRecordingStatus } from 'src/logic-functions/domain/map-recall-status-code-to-call-recording-status.util'; -import { normalizeRecallTimestamp } from 'src/logic-functions/recall-api/normalize-recall-timestamp.util'; - -export type RecallBotConvergence = { - status: CallRecordingStatus | undefined; - failureReason: string | undefined; - startedAt: string | undefined; - endedAt: string | undefined; - externalRecordingId: string | undefined; - isRecallRecordingDone: boolean; -}; - -type RecallBotStatusChange = { - code: string; - createdAt: string | undefined; -}; - -// Derives the state a full webhook history would have produced from GET /bot. -export const extractRecallBotConvergence = ( - bot: Record, -): RecallBotConvergence => { - const statusChanges = extractStatusChanges(bot); - const latestStatusChange = getLatestStatusChange(statusChanges); - const status = mapRecallStatusCodeToCallRecordingStatus( - latestStatusChange?.code, - ); - const recording = extractFirstRecording(bot); - - return { - status, - failureReason: - status === CallRecordingStatus.FAILED - ? latestStatusChange?.code - : undefined, - startedAt: normalizeRecallTimestamp( - recording?.startedAt ?? - findStatusChangeTimestamp(statusChanges, 'in_call_recording'), - ), - endedAt: normalizeRecallTimestamp( - recording?.completedAt ?? - findStatusChangeTimestamp(statusChanges, 'call_ended'), - ), - externalRecordingId: recording?.id, - isRecallRecordingDone: - !isUndefined(recording?.completedAt) || - statusChanges.some((statusChange) => statusChange.code === 'done'), - }; -}; - -const extractStatusChanges = ( - bot: Record, -): RecallBotStatusChange[] => { - if (!isArray(bot.status_changes)) { - return []; - } - - return bot.status_changes.flatMap((statusChange: unknown) => { - const code = getString(asRecord(statusChange)?.code); - - if (isUndefined(code)) { - return []; - } - - return [{ code, createdAt: getString(asRecord(statusChange)?.created_at) }]; - }); -}; - -const getLatestStatusChange = ( - statusChanges: RecallBotStatusChange[], -): RecallBotStatusChange | undefined => - statusChanges.reduce( - (latestStatusChange, statusChange) => { - if (isUndefined(latestStatusChange)) { - return statusChange; - } - - const statusChangeTime = getStatusChangeTime(statusChange); - const latestStatusChangeTime = getStatusChangeTime(latestStatusChange); - - if ( - isUndefined(statusChangeTime) && - isUndefined(latestStatusChangeTime) - ) { - return statusChange; - } - - if (isUndefined(statusChangeTime)) { - return latestStatusChange; - } - - if (isUndefined(latestStatusChangeTime)) { - return statusChange; - } - - return statusChangeTime >= latestStatusChangeTime - ? statusChange - : latestStatusChange; - }, - undefined, - ); - -const getStatusChangeTime = ( - statusChange: RecallBotStatusChange, -): number | undefined => { - const normalizedTimestamp = normalizeRecallTimestamp(statusChange.createdAt); - - if (isUndefined(normalizedTimestamp)) { - return undefined; - } - - return new Date(normalizedTimestamp).getTime(); -}; - -const extractFirstRecording = ( - bot: Record, -): - | { - id: string | undefined; - startedAt: string | undefined; - completedAt: string | undefined; - } - | undefined => { - if (!isArray(bot.recordings)) { - return undefined; - } - - const recording = asRecord(bot.recordings[0]); - - if (isUndefined(recording)) { - return undefined; - } - - return { - id: getString(recording.id), - startedAt: getString(recording.started_at), - completedAt: getString(recording.completed_at), - }; -}; - -const findStatusChangeTimestamp = ( - statusChanges: RecallBotStatusChange[], - code: string, -): string | undefined => - statusChanges.find((statusChange) => statusChange.code === code)?.createdAt; diff --git a/packages/twenty-apps/public/twenty-meeting-bot/src/logic-functions/recall-api/extract-recall-bot-id.util.ts b/packages/twenty-apps/public/twenty-meeting-bot/src/logic-functions/recall-api/extract-recall-bot-id.util.ts deleted file mode 100644 index 165ba2d327bc3..0000000000000 --- a/packages/twenty-apps/public/twenty-meeting-bot/src/logic-functions/recall-api/extract-recall-bot-id.util.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { getString } from 'src/logic-functions/utils/get-string.util'; - -export type RecallBotResponse = { - id?: unknown; - bot_id?: unknown; -}; - -export const extractRecallBotId = ( - response: RecallBotResponse | undefined, -): string | undefined => getString(response?.id) ?? getString(response?.bot_id); diff --git a/packages/twenty-apps/public/twenty-meeting-bot/src/logic-functions/recall-api/extract-recall-media-urls.util.ts b/packages/twenty-apps/public/twenty-meeting-bot/src/logic-functions/recall-api/extract-recall-media-urls.util.ts deleted file mode 100644 index 83d4bdba7c209..0000000000000 --- a/packages/twenty-apps/public/twenty-meeting-bot/src/logic-functions/recall-api/extract-recall-media-urls.util.ts +++ /dev/null @@ -1,30 +0,0 @@ -import { asRecord } from 'src/logic-functions/utils/as-record.util'; -import { getRecordAtPath } from 'src/logic-functions/utils/get-record-at-path.util'; -import { getString } from 'src/logic-functions/utils/get-string.util'; - -export type RecallMediaUrls = { - videoUrl: string | undefined; - audioUrl: string | undefined; -}; - -// Pre-signed URLs expire within hours; always re-extract from a fresh GET /recording. -export const extractRecallMediaUrls = ( - recording: Record, -): RecallMediaUrls => { - const mediaShortcuts = asRecord(recording.media_shortcuts); - - return { - videoUrl: extractArtifactDownloadUrl(mediaShortcuts, 'video_mixed'), - audioUrl: extractArtifactDownloadUrl(mediaShortcuts, 'audio_mixed'), - }; -}; - -// v1.11 exposes download_url flat on the artifact; older artifacts nest it under data. -const extractArtifactDownloadUrl = ( - mediaShortcuts: Record | undefined, - artifactKey: string, -): string | undefined => - getString(getRecordAtPath(mediaShortcuts, [artifactKey, 'download_url'])) ?? - getString( - getRecordAtPath(mediaShortcuts, [artifactKey, 'data', 'download_url']), - ); diff --git a/packages/twenty-apps/public/twenty-meeting-bot/src/logic-functions/recall-api/get-recall-api-config.util.ts b/packages/twenty-apps/public/twenty-meeting-bot/src/logic-functions/recall-api/get-recall-api-config.util.ts deleted file mode 100644 index 3bac96a932918..0000000000000 --- a/packages/twenty-apps/public/twenty-meeting-bot/src/logic-functions/recall-api/get-recall-api-config.util.ts +++ /dev/null @@ -1,59 +0,0 @@ -import { isUndefined } from '@sniptt/guards'; - -import { DEFAULT_MEETING_BOT_NAME } from 'src/logic-functions/constants/default-meeting-bot-name'; -import { DEFAULT_RECALL_REGION } from 'src/logic-functions/constants/default-recall-region'; -import { MEETING_BOT_NAME_ENV_VAR_NAME } from 'src/logic-functions/constants/meeting-bot-name-env-var-name'; -import { RECALL_API_KEY_ENV_VAR_NAME } from 'src/logic-functions/constants/recall-api-key-env-var-name'; -import { RECALL_REGION_ENV_VAR_NAME } from 'src/logic-functions/constants/recall-region-env-var-name'; -import { getApplicationVariableValue } from 'src/logic-functions/utils/get-application-variable-value.util'; -import { isNonEmptyString } from 'src/logic-functions/utils/is-non-empty-string.util'; - -export type RecallApiConfig = { - apiKey: string; - baseUrl: string; - botName: string; -}; - -export const getRecallApiConfig = (): - | { - success: true; - config: RecallApiConfig; - } - | { - success: false; - error: string; - } => { - const apiKey = normalizeOptionalString( - getApplicationVariableValue(RECALL_API_KEY_ENV_VAR_NAME), - ); - - if (isUndefined(apiKey)) { - return { - success: false, - error: - 'RECALL_API_KEY server variable is not set. A server admin must set it on the Twenty Meeting Bot application registration before scheduling bots.', - }; - } - - const region = - normalizeOptionalString( - getApplicationVariableValue(RECALL_REGION_ENV_VAR_NAME), - ) ?? DEFAULT_RECALL_REGION; - const botName = - normalizeOptionalString( - getApplicationVariableValue(MEETING_BOT_NAME_ENV_VAR_NAME), - ) ?? DEFAULT_MEETING_BOT_NAME; - - return { - success: true, - config: { - apiKey, - baseUrl: `https://${region}.recall.ai/api/v1`, - botName, - }, - }; -}; - -const normalizeOptionalString = ( - value: string | undefined, -): string | undefined => (isNonEmptyString(value) ? value.trim() : undefined); diff --git a/packages/twenty-apps/public/twenty-meeting-bot/src/logic-functions/recall-api/get-recall-bot.util.ts b/packages/twenty-apps/public/twenty-meeting-bot/src/logic-functions/recall-api/get-recall-bot.util.ts deleted file mode 100644 index 7772622d6d973..0000000000000 --- a/packages/twenty-apps/public/twenty-meeting-bot/src/logic-functions/recall-api/get-recall-bot.util.ts +++ /dev/null @@ -1,42 +0,0 @@ -import { type RecallBotOperationFailure } from 'src/logic-functions/types/recall-bot-operation-result.type'; -import { asRecord } from 'src/logic-functions/utils/as-record.util'; -import { getRecallApiConfig } from 'src/logic-functions/recall-api/get-recall-api-config.util'; -import { recallBotApiRequest } from 'src/logic-functions/recall-api/recall-bot-api-request.util'; - -type GetRecallBotResult = - | { ok: true; bot: Record } - | RecallBotOperationFailure; - -export const getRecallBot = async ({ - externalBotId, -}: { - externalBotId: string; -}): Promise => { - const configResult = getRecallApiConfig(); - - if (!configResult.success) { - return { ok: false, status: null, errorMessage: configResult.error }; - } - - const result = await recallBotApiRequest>({ - config: configResult.config, - path: `/bot/${externalBotId}/`, - method: 'GET', - }); - - if (!result.ok) { - return result; - } - - const bot = asRecord(result.data); - - if (bot === undefined) { - return { - ok: false, - status: result.status, - errorMessage: 'Recall API returned an empty bot response', - }; - } - - return { ok: true, bot }; -}; diff --git a/packages/twenty-apps/public/twenty-meeting-bot/src/logic-functions/recall-api/get-recall-recording.util.ts b/packages/twenty-apps/public/twenty-meeting-bot/src/logic-functions/recall-api/get-recall-recording.util.ts deleted file mode 100644 index a81133ec09bcb..0000000000000 --- a/packages/twenty-apps/public/twenty-meeting-bot/src/logic-functions/recall-api/get-recall-recording.util.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { type RecallBotOperationFailure } from 'src/logic-functions/types/recall-bot-operation-result.type'; -import { getRecallApiConfig } from 'src/logic-functions/recall-api/get-recall-api-config.util'; -import { recallBotApiRequest } from 'src/logic-functions/recall-api/recall-bot-api-request.util'; - -type GetRecallRecordingResult = - | { ok: true; recording: Record } - | RecallBotOperationFailure; - -export const getRecallRecording = async ({ - externalRecordingId, -}: { - externalRecordingId: string; -}): Promise => { - const configResult = getRecallApiConfig(); - - if (!configResult.success) { - return { ok: false, status: null, errorMessage: configResult.error }; - } - - const result = await recallBotApiRequest>({ - config: configResult.config, - path: `/recording/${externalRecordingId}/`, - method: 'GET', - }); - - if (!result.ok) { - return result; - } - - return { ok: true, recording: result.data ?? {} }; -}; diff --git a/packages/twenty-apps/public/twenty-meeting-bot/src/logic-functions/recall-api/list-recall-transcripts.util.ts b/packages/twenty-apps/public/twenty-meeting-bot/src/logic-functions/recall-api/list-recall-transcripts.util.ts deleted file mode 100644 index 21c6d925f2104..0000000000000 --- a/packages/twenty-apps/public/twenty-meeting-bot/src/logic-functions/recall-api/list-recall-transcripts.util.ts +++ /dev/null @@ -1,141 +0,0 @@ -import { isArray, isUndefined } from '@sniptt/guards'; - -import { type RecallBotOperationFailure } from 'src/logic-functions/types/recall-bot-operation-result.type'; -import { asRecord } from 'src/logic-functions/utils/as-record.util'; -import { getString } from 'src/logic-functions/utils/get-string.util'; -import { getRecallApiConfig } from 'src/logic-functions/recall-api/get-recall-api-config.util'; -import { recallBotApiRequest } from 'src/logic-functions/recall-api/recall-bot-api-request.util'; -import { type RecallTranscriptSummary } from 'src/logic-functions/recall-api/recall-transcript-summary.type'; - -type ListRecallTranscriptsResult = - | { ok: true; transcripts: RecallTranscriptSummary[] } - | RecallBotOperationFailure; - -type RecallTranscriptListResponse = { - next?: unknown; - results?: unknown; -}; - -const RECALL_TRANSCRIPT_LIST_MAX_PAGES = 10; - -export const listRecallTranscripts = async ({ - externalRecordingId, -}: { - externalRecordingId: string; -}): Promise => { - const configResult = getRecallApiConfig(); - - if (!configResult.success) { - return { ok: false, status: null, errorMessage: configResult.error }; - } - - const transcripts: RecallTranscriptSummary[] = []; - let path: string | undefined = buildListRecallTranscriptsPath({ - externalRecordingId, - }); - - for ( - let pageIndex = 0; - !isUndefined(path) && pageIndex < RECALL_TRANSCRIPT_LIST_MAX_PAGES; - pageIndex++ - ) { - const result = await recallBotApiRequest({ - config: configResult.config, - path, - method: 'GET', - }); - - if (!result.ok) { - return result; - } - - const pageTranscripts = extractRecallTranscriptSummaries(result.data); - - if (isUndefined(pageTranscripts)) { - return { - ok: false, - status: result.status, - errorMessage: 'Recall API returned malformed transcript list', - }; - } - - transcripts.push(...pageTranscripts); - path = extractNextPath(result.data, configResult.config.baseUrl); - } - - if (!isUndefined(path)) { - return { - ok: false, - status: null, - errorMessage: `Recall transcript list exceeded ${RECALL_TRANSCRIPT_LIST_MAX_PAGES} pages`, - }; - } - - return { ok: true, transcripts }; -}; - -const buildListRecallTranscriptsPath = ({ - externalRecordingId, -}: { - externalRecordingId: string; -}): string => { - const searchParams = new URLSearchParams({ - recording_id: externalRecordingId, - }); - - return `/transcript/?${searchParams.toString()}`; -}; - -const extractRecallTranscriptSummaries = ( - response: RecallTranscriptListResponse | undefined, -): RecallTranscriptSummary[] | undefined => { - if (!isArray(response?.results)) { - return undefined; - } - - const transcripts: RecallTranscriptSummary[] = []; - - for (const result of response.results) { - const transcript = extractRecallTranscriptSummary(result); - - if (isUndefined(transcript)) { - return undefined; - } - - transcripts.push(transcript); - } - - return transcripts; -}; - -const extractRecallTranscriptSummary = ( - transcript: unknown, -): RecallTranscriptSummary | undefined => { - const transcriptRecord = asRecord(transcript); - const transcriptId = getString(transcriptRecord?.id); - - if (isUndefined(transcriptRecord) || isUndefined(transcriptId)) { - return undefined; - } - - const status = asRecord(transcriptRecord.status); - - return { - id: transcriptId, - statusCode: getString(status?.code), - statusSubCode: getString(status?.sub_code), - }; -}; - -const extractNextPath = ( - response: RecallTranscriptListResponse | undefined, - baseUrl: string, -): string | undefined => { - const nextPage = getString(response?.next); - - if (isUndefined(nextPage) || !nextPage.startsWith(baseUrl)) { - return undefined; - } - - return nextPage.slice(baseUrl.length); -}; diff --git a/packages/twenty-apps/public/twenty-meeting-bot/src/logic-functions/recall-api/list-scheduled-recall-bots.util.ts b/packages/twenty-apps/public/twenty-meeting-bot/src/logic-functions/recall-api/list-scheduled-recall-bots.util.ts deleted file mode 100644 index 19fe22fae3645..0000000000000 --- a/packages/twenty-apps/public/twenty-meeting-bot/src/logic-functions/recall-api/list-scheduled-recall-bots.util.ts +++ /dev/null @@ -1,106 +0,0 @@ -import { isString, isUndefined } from '@sniptt/guards'; - -import { type RecallBotOperationFailure } from 'src/logic-functions/types/recall-bot-operation-result.type'; -import { asRecord } from 'src/logic-functions/utils/as-record.util'; -import { getRecallApiConfig } from 'src/logic-functions/recall-api/get-recall-api-config.util'; -import { recallBotApiRequest } from 'src/logic-functions/recall-api/recall-bot-api-request.util'; - -export type RecallScheduledBot = { - id: string; - metadata: Record; -}; - -type RecallBotListResponse = { - next?: unknown; - results?: unknown; -}; - -type ListScheduledRecallBotsResult = - | { ok: true; bots: RecallScheduledBot[] } - | RecallBotOperationFailure; - -const RECALL_BOT_LIST_MAX_PAGES = 10; - -export const listScheduledRecallBots = async ({ - joinAtAfter, - joinAtBefore, -}: { - joinAtAfter: string; - joinAtBefore: string; -}): Promise => { - const configResult = getRecallApiConfig(); - - if (!configResult.success) { - return { ok: false, status: null, errorMessage: configResult.error }; - } - - const bots: RecallScheduledBot[] = []; - let path: string | undefined = `/bot/?join_at_after=${encodeURIComponent( - joinAtAfter, - )}&join_at_before=${encodeURIComponent(joinAtBefore)}`; - - for ( - let pageIndex = 0; - !isUndefined(path) && pageIndex < RECALL_BOT_LIST_MAX_PAGES; - pageIndex++ - ) { - const result = await recallBotApiRequest({ - config: configResult.config, - path, - method: 'GET', - }); - - if (!result.ok) { - return result; - } - - bots.push(...extractRecallBots(result.data)); - path = extractNextPath(result.data, configResult.config.baseUrl); - } - - if (!isUndefined(path)) { - return { - ok: false, - status: null, - errorMessage: `Recall bot list exceeded ${RECALL_BOT_LIST_MAX_PAGES} pages`, - }; - } - - return { ok: true, bots }; -}; - -const extractRecallBots = ( - response: RecallBotListResponse | undefined, -): RecallScheduledBot[] => { - if (!Array.isArray(response?.results)) { - return []; - } - - return response.results.flatMap((candidate: unknown) => { - const bot = asRecord(candidate); - - if (isUndefined(bot) || !isString(bot.id)) { - return []; - } - - return [ - { - id: bot.id, - metadata: asRecord(bot.metadata) ?? {}, - }, - ]; - }); -}; - -const extractNextPath = ( - response: RecallBotListResponse | undefined, - baseUrl: string, -): string | undefined => { - const next = response?.next; - - if (!isString(next) || !next.startsWith(baseUrl)) { - return undefined; - } - - return next.slice(baseUrl.length); -}; diff --git a/packages/twenty-apps/public/twenty-meeting-bot/src/logic-functions/recall-api/normalize-recall-timestamp.util.ts b/packages/twenty-apps/public/twenty-meeting-bot/src/logic-functions/recall-api/normalize-recall-timestamp.util.ts deleted file mode 100644 index 1c2dd44cde49d..0000000000000 --- a/packages/twenty-apps/public/twenty-meeting-bot/src/logic-functions/recall-api/normalize-recall-timestamp.util.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { isUndefined } from '@sniptt/guards'; - -// Twenty rejects Recall's microsecond precision; truncate to millisecond ISO. -export const normalizeRecallTimestamp = ( - value: string | undefined, -): string | undefined => { - if (isUndefined(value)) { - return undefined; - } - - const parsed = new Date(value); - - return Number.isNaN(parsed.getTime()) ? undefined : parsed.toISOString(); -}; diff --git a/packages/twenty-apps/public/twenty-meeting-bot/src/logic-functions/recall-api/parse-recall-webhook-event.util.ts b/packages/twenty-apps/public/twenty-meeting-bot/src/logic-functions/recall-api/parse-recall-webhook-event.util.ts deleted file mode 100644 index e1752ac8c80ab..0000000000000 --- a/packages/twenty-apps/public/twenty-meeting-bot/src/logic-functions/recall-api/parse-recall-webhook-event.util.ts +++ /dev/null @@ -1,104 +0,0 @@ -import { isUndefined } from '@sniptt/guards'; - -import { asRecord } from 'src/logic-functions/utils/as-record.util'; -import { getRecordAtPath } from 'src/logic-functions/utils/get-record-at-path.util'; -import { getString } from 'src/logic-functions/utils/get-string.util'; -import { normalizeRecallTimestamp } from 'src/logic-functions/recall-api/normalize-recall-timestamp.util'; - -export type RecallWebhookBody = { - event?: unknown; - type?: unknown; - data?: unknown; - bot?: unknown; -}; - -export type RecallWebhookEvent = { - event: string; - statusCode: string | undefined; - statusTimestamp: string | undefined; - externalBotId: string | undefined; - externalRecordingId: string | undefined; - callRecordingIdFromMetadata: string | undefined; - recordingStartedAt: string | undefined; - recordingEndedAt: string | undefined; - transcriptId: string | undefined; - transcriptFailureSubCode: string | undefined; -}; - -// The only reader of raw webhook payloads; Recall delivers several body shapes per event family. -export const parseRecallWebhookEvent = ( - body: RecallWebhookBody, -): RecallWebhookEvent | undefined => { - const event = getString(body.event) ?? getString(body.type); - - if (isUndefined(event)) { - return undefined; - } - - const data = asRecord(body.data); - const bot = asRecord(body.bot); - - return { - event, - statusCode: - getString(getRecordAtPath(data, ['status', 'code'])) ?? - getString(getRecordAtPath(data, ['data', 'code'])) ?? - getString(getRecordAtPath(bot, ['status', 'code'])) ?? - getStatusCodeFromEventName(event), - statusTimestamp: normalizeRecallTimestamp( - getString(getRecordAtPath(data, ['status', 'created_at'])) ?? - getString(getRecordAtPath(data, ['data', 'updated_at'])) ?? - getString(getRecordAtPath(bot, ['status', 'created_at'])), - ), - externalBotId: - getString(data?.bot_id) ?? - getString(getRecordAtPath(data, ['bot', 'id'])) ?? - getString(getRecordAtPath(data, ['recording', 'bot_id'])) ?? - getString(getRecordAtPath(data, ['recording', 'bot', 'id'])) ?? - getString(bot?.id), - externalRecordingId: - getString(getRecordAtPath(data, ['status', 'recording_id'])) ?? - getString(getRecordAtPath(data, ['recording', 'id'])) ?? - getString(data?.recording_id), - callRecordingIdFromMetadata: extractCallRecordingIdFromMetadata({ - data, - bot, - }), - recordingStartedAt: normalizeRecallTimestamp( - getString(getRecordAtPath(data, ['recording', 'started_at'])), - ), - recordingEndedAt: normalizeRecallTimestamp( - getString(getRecordAtPath(data, ['recording', 'completed_at'])), - ), - transcriptId: getString(getRecordAtPath(data, ['transcript', 'id'])), - transcriptFailureSubCode: getString( - getRecordAtPath(data, ['status', 'sub_code']), - ), - }; -}; - -const getStatusCodeFromEventName = (event: string): string | undefined => { - if (!event.startsWith('bot.')) { - return undefined; - } - - const statusCode = event.slice('bot.'.length); - - return statusCode === 'status_change' ? undefined : statusCode; -}; - -const extractCallRecordingIdFromMetadata = ({ - data, - bot, -}: { - data: Record | undefined; - bot: Record | undefined; -}): string | undefined => { - const metadata = - asRecord(bot?.metadata) ?? - asRecord(getRecordAtPath(data, ['bot', 'metadata'])) ?? - asRecord(getRecordAtPath(data, ['recording', 'metadata'])) ?? - asRecord(data?.metadata); - - return getString(metadata?.twentyCallRecordingId); -}; diff --git a/packages/twenty-apps/public/twenty-meeting-bot/src/logic-functions/recall-api/recall-bot-api-request.util.ts b/packages/twenty-apps/public/twenty-meeting-bot/src/logic-functions/recall-api/recall-bot-api-request.util.ts deleted file mode 100644 index 62374f7f4bf2d..0000000000000 --- a/packages/twenty-apps/public/twenty-meeting-bot/src/logic-functions/recall-api/recall-bot-api-request.util.ts +++ /dev/null @@ -1,165 +0,0 @@ -import { isUndefined } from '@sniptt/guards'; - -import { RECALL_API_MAX_ATTEMPTS } from 'src/logic-functions/constants/recall-api-max-attempts'; -import { RECALL_API_RETRY_DELAY_MS } from 'src/logic-functions/constants/recall-api-retry-delay-ms'; -import { type RecallApiConfig } from 'src/logic-functions/recall-api/get-recall-api-config.util'; - -type RecallBotApiRequestArgs = { - config: RecallApiConfig; - path: string; - method: 'GET' | 'POST' | 'PATCH' | 'DELETE'; - body?: unknown; - allowNotFound?: boolean; - maxAttempts?: number; -}; - -type RecallBotApiRequestResult = - | { - ok: true; - status: number; - data: TData; - } - | { - ok: false; - status: number | null; - errorMessage: string; - }; - -// Bot creates tolerate retries because duplicates stay unclaimed and get reaped. -// Callers that cannot retry idempotently can lower maxAttempts. -export const recallBotApiRequest = async ( - requestArgs: RecallBotApiRequestArgs, -): Promise> => { - const maxAttempts = requestArgs.maxAttempts ?? RECALL_API_MAX_ATTEMPTS; - - for (let attemptNumber = 1; ; attemptNumber++) { - const { result, isRetryable } = - await performRecallBotApiRequestAttempt(requestArgs); - - if (!isRetryable || attemptNumber >= maxAttempts) { - return result; - } - - await sleep(RECALL_API_RETRY_DELAY_MS * attemptNumber); - } -}; - -const performRecallBotApiRequestAttempt = async ({ - config, - path, - method, - body, - allowNotFound = false, -}: RecallBotApiRequestArgs): Promise<{ - result: RecallBotApiRequestResult; - isRetryable: boolean; -}> => { - let response: Response; - - try { - response = await fetch(`${config.baseUrl}${path}`, { - method, - headers: { - Authorization: buildRecallApiAuthorizationHeader(config.apiKey), - ...(isUndefined(body) ? {} : { 'Content-Type': 'application/json' }), - }, - ...(isUndefined(body) ? {} : { body: JSON.stringify(body) }), - }); - } catch (error) { - return { - isRetryable: true, - result: { - ok: false, - status: null, - errorMessage: `Recall API request failed: ${ - error instanceof Error ? error.message : String(error) - }`, - }, - }; - } - - if (allowNotFound && response.status === 404) { - return { - isRetryable: false, - result: { - ok: true, - status: response.status, - data: undefined as TData, - }, - }; - } - - if (response.status === 204) { - return { - isRetryable: false, - result: { - ok: true, - status: response.status, - data: undefined as TData, - }, - }; - } - - if (!response.ok) { - return { - isRetryable: isRetryableRecallApiStatus(response.status), - result: { - ok: false, - status: response.status, - errorMessage: await extractRecallApiErrorMessage(response), - }, - }; - } - - try { - return { - isRetryable: false, - result: { - ok: true, - status: response.status, - data: (await response.json()) as TData, - }, - }; - } catch (error) { - return { - isRetryable: false, - result: { - ok: false, - status: response.status, - errorMessage: `Recall API returned a non-JSON response: ${ - error instanceof Error ? error.message : String(error) - }`, - }, - }; - } -}; - -const isRetryableRecallApiStatus = (status: number): boolean => - status === 429 || status >= 500; - -const sleep = (delayMs: number): Promise => - new Promise((resolve) => { - setTimeout(resolve, delayMs); - }); - -const buildRecallApiAuthorizationHeader = (apiKey: string): string => { - const trimmedApiKey = apiKey.trim(); - - return trimmedApiKey.toLowerCase().startsWith('token ') - ? trimmedApiKey - : `Token ${trimmedApiKey}`; -}; - -const extractRecallApiErrorMessage = async ( - response: Response, -): Promise => { - const fallback = `Recall API responded with HTTP ${response.status}`; - - try { - const body = (await response.json()) as unknown; - - return `${fallback}: ${JSON.stringify(body)}`; - } catch { - return fallback; - } -}; diff --git a/packages/twenty-apps/public/twenty-meeting-bot/src/logic-functions/recall-api/recall-transcript-summary.type.ts b/packages/twenty-apps/public/twenty-meeting-bot/src/logic-functions/recall-api/recall-transcript-summary.type.ts deleted file mode 100644 index 32f2683c897d9..0000000000000 --- a/packages/twenty-apps/public/twenty-meeting-bot/src/logic-functions/recall-api/recall-transcript-summary.type.ts +++ /dev/null @@ -1,5 +0,0 @@ -export type RecallTranscriptSummary = { - id: string; - statusCode: string | undefined; - statusSubCode: string | undefined; -}; diff --git a/packages/twenty-apps/public/twenty-meeting-bot/src/logic-functions/recall-api/reschedule-recall-bot.util.ts b/packages/twenty-apps/public/twenty-meeting-bot/src/logic-functions/recall-api/reschedule-recall-bot.util.ts deleted file mode 100644 index cf6cc2d0f8db6..0000000000000 --- a/packages/twenty-apps/public/twenty-meeting-bot/src/logic-functions/recall-api/reschedule-recall-bot.util.ts +++ /dev/null @@ -1,54 +0,0 @@ -import { isUndefined } from '@sniptt/guards'; - -import { getRecallBotAutomaticLeave } from 'src/logic-functions/constants/recall-bot-automatic-leave'; -import { getRecallBotRecordingConfig } from 'src/logic-functions/constants/recall-bot-recording-config'; -import { type RecallBotScheduleResult } from 'src/logic-functions/types/recall-bot-operation-result.type'; -import { - extractRecallBotId, - type RecallBotResponse, -} from 'src/logic-functions/recall-api/extract-recall-bot-id.util'; -import { getRecallApiConfig } from 'src/logic-functions/recall-api/get-recall-api-config.util'; -import { recallBotApiRequest } from 'src/logic-functions/recall-api/recall-bot-api-request.util'; -import { type ScheduleRecallBotArgs } from 'src/logic-functions/recall-api/schedule-recall-bot.util'; - -type RescheduleRecallBotArgs = ScheduleRecallBotArgs & { - externalBotId: string; -}; - -export const rescheduleRecallBot = async ({ - externalBotId, - meetingUrl, - joinAt, - metadata, -}: RescheduleRecallBotArgs): Promise => { - const configResult = getRecallApiConfig(); - - if (!configResult.success) { - return { ok: false, status: null, errorMessage: configResult.error }; - } - - const automaticLeave = getRecallBotAutomaticLeave(); - - const result = await recallBotApiRequest({ - config: configResult.config, - path: `/bot/${externalBotId}/`, - method: 'PATCH', - body: { - meeting_url: meetingUrl, - join_at: joinAt, - bot_name: configResult.config.botName, - ...(isUndefined(automaticLeave) ? {} : { automatic_leave: automaticLeave }), - recording_config: getRecallBotRecordingConfig(), - metadata, - }, - }); - - if (!result.ok) { - return result; - } - - return { - ok: true, - externalBotId: extractRecallBotId(result.data) ?? externalBotId, - }; -}; diff --git a/packages/twenty-apps/public/twenty-meeting-bot/src/logic-functions/recall-api/retrieve-recall-transcript.util.ts b/packages/twenty-apps/public/twenty-meeting-bot/src/logic-functions/recall-api/retrieve-recall-transcript.util.ts deleted file mode 100644 index a096da7c62bbc..0000000000000 --- a/packages/twenty-apps/public/twenty-meeting-bot/src/logic-functions/recall-api/retrieve-recall-transcript.util.ts +++ /dev/null @@ -1,71 +0,0 @@ -import { isUndefined } from '@sniptt/guards'; - -import { type RecallBotOperationFailure } from 'src/logic-functions/types/recall-bot-operation-result.type'; -import { asRecord } from 'src/logic-functions/utils/as-record.util'; -import { getRecallApiConfig } from 'src/logic-functions/recall-api/get-recall-api-config.util'; -import { getString } from 'src/logic-functions/utils/get-string.util'; -import { recallBotApiRequest } from 'src/logic-functions/recall-api/recall-bot-api-request.util'; - -export type RecallTranscriptDetails = { - downloadUrl: string | undefined; - statusCode: string | undefined; - statusSubCode: string | undefined; -}; - -type RetrieveRecallTranscriptResult = - | { ok: true; transcript: RecallTranscriptDetails } - | RecallBotOperationFailure; - -export const retrieveRecallTranscript = async ({ - transcriptId, -}: { - transcriptId: string; -}): Promise => { - const configResult = getRecallApiConfig(); - - if (!configResult.success) { - return { ok: false, status: null, errorMessage: configResult.error }; - } - - const result = await recallBotApiRequest>({ - config: configResult.config, - path: `/transcript/${transcriptId}/`, - method: 'GET', - }); - - if (!result.ok) { - return result; - } - - const transcript = extractRecallTranscriptDetails(result.data); - - if (isMalformedRecallTranscriptDetails(transcript)) { - return { - ok: false, - status: result.status, - errorMessage: 'Recall API returned malformed transcript details', - }; - } - - return { ok: true, transcript }; -}; - -const extractRecallTranscriptDetails = ( - response: Record | undefined, -): RecallTranscriptDetails => { - const data = asRecord(response?.data); - const status = asRecord(response?.status); - - return { - downloadUrl: getString(data?.download_url), - statusCode: getString(status?.code), - statusSubCode: getString(status?.sub_code), - }; -}; - -const isMalformedRecallTranscriptDetails = ({ - downloadUrl, - statusCode, -}: RecallTranscriptDetails): boolean => - (isUndefined(downloadUrl) && isUndefined(statusCode)) || - (isUndefined(downloadUrl) && statusCode === 'done'); diff --git a/packages/twenty-apps/public/twenty-meeting-bot/src/logic-functions/recall-api/schedule-recall-bot.util.ts b/packages/twenty-apps/public/twenty-meeting-bot/src/logic-functions/recall-api/schedule-recall-bot.util.ts deleted file mode 100644 index 8de80b44e1d1d..0000000000000 --- a/packages/twenty-apps/public/twenty-meeting-bot/src/logic-functions/recall-api/schedule-recall-bot.util.ts +++ /dev/null @@ -1,66 +0,0 @@ -import { isUndefined } from '@sniptt/guards'; - -import { getRecallBotAutomaticLeave } from 'src/logic-functions/constants/recall-bot-automatic-leave'; -import { getRecallBotRecordingConfig } from 'src/logic-functions/constants/recall-bot-recording-config'; -import { type RecallBotMetadata } from 'src/logic-functions/types/recall-bot-metadata.type'; -import { type RecallBotScheduleResult } from 'src/logic-functions/types/recall-bot-operation-result.type'; -import { - extractRecallBotId, - type RecallBotResponse, -} from 'src/logic-functions/recall-api/extract-recall-bot-id.util'; -import { getRecallApiConfig } from 'src/logic-functions/recall-api/get-recall-api-config.util'; -import { recallBotApiRequest } from 'src/logic-functions/recall-api/recall-bot-api-request.util'; - -export type ScheduleRecallBotArgs = { - meetingUrl: string; - joinAt: string; - metadata: RecallBotMetadata; -}; - -export const scheduleRecallBot = async ({ - meetingUrl, - joinAt, - metadata, -}: ScheduleRecallBotArgs): Promise => { - const configResult = getRecallApiConfig(); - - if (!configResult.success) { - return { ok: false, status: null, errorMessage: configResult.error }; - } - - const automaticLeave = getRecallBotAutomaticLeave(); - - const result = await recallBotApiRequest({ - config: configResult.config, - path: '/bot/', - method: 'POST', - body: { - meeting_url: meetingUrl, - join_at: joinAt, - bot_name: configResult.config.botName, - ...(isUndefined(automaticLeave) ? {} : { automatic_leave: automaticLeave }), - recording_config: getRecallBotRecordingConfig(), - metadata, - }, - }); - - if (!result.ok) { - return result; - } - - const externalBotId = extractRecallBotId(result.data); - - if (isUndefined(externalBotId)) { - return { - ok: false, - status: null, - errorMessage: - 'Recall API created a bot but the response did not include a bot id', - }; - } - - return { - ok: true, - externalBotId, - }; -}; diff --git a/packages/twenty-apps/public/twenty-meeting-bot/src/logic-functions/recall-api/verify-recall-webhook-signature.util.ts b/packages/twenty-apps/public/twenty-meeting-bot/src/logic-functions/recall-api/verify-recall-webhook-signature.util.ts deleted file mode 100644 index f42106aae5f50..0000000000000 --- a/packages/twenty-apps/public/twenty-meeting-bot/src/logic-functions/recall-api/verify-recall-webhook-signature.util.ts +++ /dev/null @@ -1,109 +0,0 @@ -import { createHmac, timingSafeEqual } from 'crypto'; - -import { isUndefined } from '@sniptt/guards'; - -const RECALL_WEBHOOK_SECRET_PREFIX = 'whsec_'; -const RECALL_WEBHOOK_TIMESTAMP_TOLERANCE_SECONDS = 5 * 60; - -export const verifyRecallWebhookSignature = ({ - rawBody, - headers, - secret, - now = new Date(), -}: { - rawBody: string; - headers: Record; - secret: string; - now?: Date; -}): { valid: true } | { valid: false; error: string } => { - if (!secret.startsWith(RECALL_WEBHOOK_SECRET_PREFIX)) { - return { - valid: false, - error: 'Webhook secret must start with whsec_', - }; - } - - const webhookId = headers['webhook-id'] ?? headers['svix-id']; - const webhookTimestamp = - headers['webhook-timestamp'] ?? headers['svix-timestamp']; - const webhookSignature = - headers['webhook-signature'] ?? headers['svix-signature']; - - if ( - isUndefined(webhookId) || - isUndefined(webhookTimestamp) || - isUndefined(webhookSignature) - ) { - return { - valid: false, - error: 'Missing webhook signature headers', - }; - } - - const webhookTimestampSeconds = Number(webhookTimestamp); - - if (!Number.isInteger(webhookTimestampSeconds)) { - return { - valid: false, - error: 'Invalid webhook timestamp', - }; - } - - const nowSeconds = Math.floor(now.getTime() / 1000); - - if ( - Math.abs(nowSeconds - webhookTimestampSeconds) > - RECALL_WEBHOOK_TIMESTAMP_TOLERANCE_SECONDS - ) { - return { - valid: false, - error: 'Webhook timestamp is outside of the allowed tolerance', - }; - } - - const secretBytes = Buffer.from( - secret.slice(RECALL_WEBHOOK_SECRET_PREFIX.length), - 'base64', - ); - const expectedSignature = createHmac('sha256', secretBytes) - .update(`${webhookId}.${webhookTimestamp}.${rawBody}`) - .digest('base64'); - const providedSignatures = webhookSignature - .split(' ') - .map((signaturePart) => signaturePart.trim()) - .filter((signaturePart) => signaturePart !== '') - .flatMap((signaturePart) => { - if (signaturePart.startsWith('v1,') || signaturePart.startsWith('v1=')) { - return [signaturePart.slice(3).trim()]; - } - - return []; - }) - .filter((signaturePart) => signaturePart !== ''); - - if (providedSignatures.length === 0) { - return { - valid: false, - error: 'Missing v1 signature', - }; - } - - const expectedSignatureBuffer = Buffer.from(expectedSignature, 'base64'); - - for (const providedSignature of providedSignatures) { - const providedSignatureBuffer = Buffer.from(providedSignature, 'base64'); - - if (providedSignatureBuffer.length !== expectedSignatureBuffer.length) { - continue; - } - - if (timingSafeEqual(providedSignatureBuffer, expectedSignatureBuffer)) { - return { valid: true }; - } - } - - return { - valid: false, - error: 'Signature verification failed', - }; -}; diff --git a/packages/twenty-apps/public/twenty-meeting-bot/src/logic-functions/recall-webhook.ts b/packages/twenty-apps/public/twenty-meeting-bot/src/logic-functions/recall-webhook.ts deleted file mode 100644 index 770989bab12af..0000000000000 --- a/packages/twenty-apps/public/twenty-meeting-bot/src/logic-functions/recall-webhook.ts +++ /dev/null @@ -1,88 +0,0 @@ -import { isNull, isUndefined } from '@sniptt/guards'; -import { CoreApiClient } from 'twenty-client-sdk/core'; -import { defineLogicFunction, type RoutePayload } from 'twenty-sdk/define'; -import { Response } from 'twenty-sdk/logic-function'; - -import { RECALL_WEBHOOK_LOGIC_FUNCTION_UNIVERSAL_IDENTIFIER } from 'src/constants/recall-webhook-logic-function-universal-identifier'; -import { RECALL_WEBHOOK_SECRET_ENV_VAR_NAME } from 'src/logic-functions/constants/recall-webhook-secret-env-var-name'; -import { handleRecallWebhook } from 'src/logic-functions/flows/handle-recall-webhook.util'; -import { type RecallWebhookBody } from 'src/logic-functions/recall-api/parse-recall-webhook-event.util'; -import { verifyRecallWebhookSignature } from 'src/logic-functions/recall-api/verify-recall-webhook-signature.util'; -import { getApplicationVariableValue } from 'src/logic-functions/utils/get-application-variable-value.util'; -import { isNonEmptyString } from 'src/logic-functions/utils/is-non-empty-string.util'; - -// Non-2xx makes Svix retry; a returned plain object would 200-ack permanently. -const rejectWebhook = (status: number, error: string): Response => { - console.error(`[twenty-meeting-bot] webhook rejected: ${error}`); - - return new Response({ error }, { status }); -}; - -export const recallWebhookRouteHandler = async ( - routePayload: RoutePayload, -): Promise => { - const webhookSecret = getApplicationVariableValue( - RECALL_WEBHOOK_SECRET_ENV_VAR_NAME, - ); - - if (!isNonEmptyString(webhookSecret)) { - return rejectWebhook( - 500, - 'RECALL_WEBHOOK_SECRET server variable is not set. A server admin must copy it from the Recall webhook endpoint settings and set it on the Twenty Meeting Bot application registration.', - ); - } - - const { rawBody } = routePayload; - - if (isUndefined(rawBody)) { - return rejectWebhook( - 500, - 'Raw request body was not forwarded by the server; cannot verify the webhook signature', - ); - } - - const signatureCheck = verifyRecallWebhookSignature({ - rawBody, - headers: routePayload.headers, - secret: webhookSecret, - }); - - if (!signatureCheck.valid) { - return rejectWebhook( - 401, - `Invalid webhook signature: ${signatureCheck.error}`, - ); - } - - if (isUndefined(routePayload.body) || isNull(routePayload.body)) { - return rejectWebhook(400, 'Webhook payload was empty'); - } - - return handleRecallWebhook({ - client: new CoreApiClient(), - body: routePayload.body, - }); -}; - -export default defineLogicFunction({ - universalIdentifier: RECALL_WEBHOOK_LOGIC_FUNCTION_UNIVERSAL_IDENTIFIER, - name: 'recall-webhook', - description: - 'Receives Recall.ai webhook events and updates the matching CallRecording lifecycle status.', - timeoutSeconds: 30, - handler: recallWebhookRouteHandler, - serverWebhookTriggerSettings: { - workspaceIdResolver: { - source: 'body', - path: 'data.bot.metadata.twentyWorkspaceId', - }, - forwardedRequestHeaders: [ - 'webhook-id', - 'webhook-timestamp', - 'webhook-signature', - 'svix-id', - 'svix-timestamp', - 'svix-signature', - ], - }, -}); diff --git a/packages/twenty-apps/public/twenty-meeting-bot/src/logic-functions/reconcile-meeting-bot-calendar-event.ts b/packages/twenty-apps/public/twenty-meeting-bot/src/logic-functions/reconcile-meeting-bot-calendar-event.ts deleted file mode 100644 index e02333038399d..0000000000000 --- a/packages/twenty-apps/public/twenty-meeting-bot/src/logic-functions/reconcile-meeting-bot-calendar-event.ts +++ /dev/null @@ -1,178 +0,0 @@ -import { isUndefined } from '@sniptt/guards'; -import { CoreApiClient } from 'twenty-client-sdk/core'; -import { - defineLogicFunction, - type DatabaseEventPayload, - type ObjectRecordBaseEvent, -} from 'twenty-sdk/define'; - -import { CALENDAR_EVENT_RECONCILIATION_LOGIC_FUNCTION_UNIVERSAL_IDENTIFIER } from 'src/constants/calendar-event-reconciliation-logic-function-universal-identifier'; -import { type RemovedMeetingBotOccurrence } from 'src/logic-functions/types/removed-meeting-bot-occurrence.type'; -import { computeRealMeetingKey } from 'src/logic-functions/domain/compute-real-meeting-key.util'; -import { getUniqueSortedIds } from 'src/logic-functions/utils/get-unique-sorted-ids.util'; -import { reconcileMeetingBotForCalendarEventIds } from 'src/logic-functions/flows/reconcile-meeting-bot.util'; - -const CALENDAR_EVENT_OBJECT_NAME = 'calendarEvent'; - -const MEETING_BOT_RELEVANT_CALENDAR_EVENT_FIELDS = [ - 'title', - 'meetingBotPreference', - 'conferenceLink', - 'startsAt', - 'endsAt', - 'isCanceled', - 'iCalUid', -]; - -const MEETING_BOT_KEY_CALENDAR_EVENT_FIELDS = [ - 'conferenceLink', - 'startsAt', - 'iCalUid', -]; - -type CalendarEventForDatabaseEvent = { - id: string; - conferenceLink?: { primaryLinkUrl?: string | null } | null; - iCalUid?: string | null; - startsAt?: string | null; -}; - -type CalendarEventDatabaseEvent = DatabaseEventPayload< - ObjectRecordBaseEvent ->; - -type CalendarEventReconciliationPayload = { - calendarEventIds: string[]; - removedOccurrences: RemovedMeetingBotOccurrence[]; -}; - -const handler = async ( - event: CalendarEventDatabaseEvent, -): Promise => { - const [objectName, action] = event.name.split('.'); - - if (objectName !== CALENDAR_EVENT_OBJECT_NAME) { - return { skipped: true, reason: 'not a calendar event' }; - } - - const reconciliationPayload = buildCalendarEventReconciliationPayload({ - event, - action, - }); - - if ( - reconciliationPayload.calendarEventIds.length === 0 && - reconciliationPayload.removedOccurrences.length === 0 - ) { - return { skipped: true, reason: 'no relevant calendar event change' }; - } - - const client = new CoreApiClient(); - const reconciliationResults = await reconcileMeetingBotForCalendarEventIds({ - client, - calendarEventIds: reconciliationPayload.calendarEventIds, - removedOccurrences: reconciliationPayload.removedOccurrences, - }); - - return { - reconciled: true, - calendarEventIds: reconciliationPayload.calendarEventIds, - removedOccurrenceCount: reconciliationPayload.removedOccurrences.length, - reconciliationResults, - }; -}; - -const buildCalendarEventReconciliationPayload = ({ - event, - action, -}: { - event: CalendarEventDatabaseEvent; - action: string | undefined; -}): CalendarEventReconciliationPayload => { - if (action === 'created') { - return { - calendarEventIds: getUniqueSortedIds([ - event.recordId, - event.properties.after?.id, - ]), - removedOccurrences: [], - }; - } - - if (action === 'updated') { - const updatedFields = event.properties.updatedFields ?? []; - - if (!hasRelevantFieldChange(updatedFields)) { - return { calendarEventIds: [], removedOccurrences: [] }; - } - - const removedOccurrence = hasKeyFieldChange(updatedFields) - ? buildRemovedOccurrence(event.properties.before) - : undefined; - - return { - calendarEventIds: getUniqueSortedIds([ - event.recordId, - event.properties.after?.id, - ]), - removedOccurrences: isUndefined(removedOccurrence) - ? [] - : [removedOccurrence], - }; - } - - if (action === 'deleted' || action === 'destroyed') { - const removedOccurrence = buildRemovedOccurrence(event.properties.before); - - return { - calendarEventIds: [], - removedOccurrences: isUndefined(removedOccurrence) - ? [] - : [removedOccurrence], - }; - } - - return { calendarEventIds: [], removedOccurrences: [] }; -}; - -const hasRelevantFieldChange = (updatedFields: string[]): boolean => - updatedFields.some((updatedField) => - MEETING_BOT_RELEVANT_CALENDAR_EVENT_FIELDS.includes(updatedField), - ); - -const hasKeyFieldChange = (updatedFields: string[]): boolean => - updatedFields.some((updatedField) => - MEETING_BOT_KEY_CALENDAR_EVENT_FIELDS.includes(updatedField), - ); - -const buildRemovedOccurrence = ( - calendarEvent: CalendarEventForDatabaseEvent | undefined, -): RemovedMeetingBotOccurrence | undefined => { - if (isUndefined(calendarEvent)) { - return undefined; - } - - return { - calendarEventId: calendarEvent.id, - realMeetingKey: computeRealMeetingKey({ - calendarEventId: calendarEvent.id, - conferenceLinkUrl: calendarEvent.conferenceLink?.primaryLinkUrl, - iCalUid: calendarEvent.iCalUid ?? undefined, - startsAt: calendarEvent.startsAt ?? undefined, - }), - startsAt: calendarEvent.startsAt ?? undefined, - }; -}; - -export default defineLogicFunction({ - universalIdentifier: - CALENDAR_EVENT_RECONCILIATION_LOGIC_FUNCTION_UNIVERSAL_IDENTIFIER, - name: 'reconcile-meeting-bot-calendar-event', - description: - 'Reconciles app-managed Recall bot recording requests when calendar events change.', - timeoutSeconds: 60, - handler, - databaseEventTriggerSettings: { - eventName: `${CALENDAR_EVENT_OBJECT_NAME}.*`, - }, -}); diff --git a/packages/twenty-apps/public/twenty-meeting-bot/src/logic-functions/reconcile-stale-bot-state.ts b/packages/twenty-apps/public/twenty-meeting-bot/src/logic-functions/reconcile-stale-bot-state.ts deleted file mode 100644 index e83b36477d712..0000000000000 --- a/packages/twenty-apps/public/twenty-meeting-bot/src/logic-functions/reconcile-stale-bot-state.ts +++ /dev/null @@ -1,110 +0,0 @@ -import { CoreApiClient } from 'twenty-client-sdk/core'; -import { defineLogicFunction } from 'twenty-sdk/define'; - -import { STALE_BOT_STATE_LOGIC_FUNCTION_UNIVERSAL_IDENTIFIER } from 'src/constants/stale-bot-state-logic-function-universal-identifier'; -import { STALE_BOT_STATE_CRON_PATTERN } from 'src/logic-functions/constants/stale-bot-state-cron-pattern'; -import { - convergeDivergedCallRecordings, -} from 'src/logic-functions/flows/converge-diverged-call-recordings.util'; -import { type ConvergeDivergedCallRecordingsResult } from 'src/logic-functions/flows/converge-diverged-call-recordings-result.type'; -import { - healCallRecordingsMissingBot, - type HealCallRecordingsMissingBotResult, -} from 'src/logic-functions/flows/heal-call-recordings-missing-bot.util'; -import { - reapOrphanedMeetingBots, - type ReapOrphanedMeetingBotsResult, -} from 'src/logic-functions/flows/reap-orphaned-meeting-bots.util'; - -// Every unwanted bot passes through this join_at window before it can attend. -const REAPER_JOIN_AT_LOOKBACK_HOURS = 4; -const REAPER_JOIN_AT_LOOKAHEAD_HOURS = 24; - -type StepFailure = { error: string }; - -const reconcileStaleBotStateHandler = async (): Promise => { - const now = new Date(); - const client = new CoreApiClient(); - - const botlessHealResult = await healCallRecordingsMissingBotSafely( - client, - now, - ); - const orphanedBotReapingResult = await reapOrphanedMeetingBotsInJoinAtWindow( - client, - now, - ); - const statusConvergenceResult = await convergeDivergedCallRecordingsSafely( - client, - now, - ); - - return { - botlessHealResult, - orphanedBotReapingResult, - statusConvergenceResult, - }; -}; - -const healCallRecordingsMissingBotSafely = async ( - client: CoreApiClient, - now: Date, -): Promise => { - try { - return await healCallRecordingsMissingBot({ client, now }); - } catch (error) { - return buildStepFailure('botless call recording healing', error); - } -}; - -const reapOrphanedMeetingBotsInJoinAtWindow = async ( - client: CoreApiClient, - now: Date, -): Promise => { - try { - return await reapOrphanedMeetingBots({ - client, - joinAtAfter: new Date( - now.getTime() - REAPER_JOIN_AT_LOOKBACK_HOURS * 60 * 60 * 1000, - ).toISOString(), - joinAtBefore: new Date( - now.getTime() + REAPER_JOIN_AT_LOOKAHEAD_HOURS * 60 * 60 * 1000, - ).toISOString(), - }); - } catch (error) { - return buildStepFailure('orphaned bot reaping', error); - } -}; - -const convergeDivergedCallRecordingsSafely = async ( - client: CoreApiClient, - now: Date, -): Promise => { - try { - return await convergeDivergedCallRecordings({ client, now }); - } catch (error) { - return buildStepFailure('call recording status convergence', error); - } -}; - -const buildStepFailure = (stepLabel: string, error: unknown): StepFailure => { - const errorMessage = error instanceof Error ? error.message : String(error); - - if (process.env.NODE_ENV !== 'test') { - console.error(`[twenty-meeting-bot] ${stepLabel} failed: ${errorMessage}`); - } - - return { error: `${stepLabel} failed` }; -}; - -export default defineLogicFunction({ - universalIdentifier: STALE_BOT_STATE_LOGIC_FUNCTION_UNIVERSAL_IDENTIFIER, - name: 'reconcile-stale-bot-state', - description: - 'Converges call recordings with Recall on a schedule: pulls stale bot statuses and overdue transcripts, finishes failed cancellations, schedules bots for recordings still missing one, and reaps unclaimed bots. Reads calendar events only to heal already-decided recordings, never to discover meetings.', - timeoutSeconds: 250, - handler: reconcileStaleBotStateHandler, - cronTriggerSettings: { - pattern: STALE_BOT_STATE_CRON_PATTERN, - }, -}); diff --git a/packages/twenty-apps/public/twenty-meeting-bot/src/logic-functions/types/calendar-event-record.type.ts b/packages/twenty-apps/public/twenty-meeting-bot/src/logic-functions/types/calendar-event-record.type.ts deleted file mode 100644 index 1871fee9a2214..0000000000000 --- a/packages/twenty-apps/public/twenty-meeting-bot/src/logic-functions/types/calendar-event-record.type.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { type MeetingBotPolicyCalendarEventInput } from 'src/logic-functions/types/meeting-bot-policy-calendar-event-input.type'; - -export type CalendarEventRecord = MeetingBotPolicyCalendarEventInput & { - title: string | undefined; -}; diff --git a/packages/twenty-apps/public/twenty-meeting-bot/src/logic-functions/types/call-recording-record.type.ts b/packages/twenty-apps/public/twenty-meeting-bot/src/logic-functions/types/call-recording-record.type.ts deleted file mode 100644 index 8d046052c8899..0000000000000 --- a/packages/twenty-apps/public/twenty-meeting-bot/src/logic-functions/types/call-recording-record.type.ts +++ /dev/null @@ -1,15 +0,0 @@ -import { type CallRecordingRequestStatus } from 'src/logic-functions/constants/call-recording-request-status'; - -// Domain read shape: absence is always undefined; null lives only on wire types. -export type CallRecordingRecord = { - id: string; - title?: string; - status?: string; - recordingRequestStatus?: CallRecordingRequestStatus; - startedAt?: string; - endedAt?: string; - calendarEventId?: string; - externalBotId?: string; - externalRecordingId?: string; - meetingBotFailureReason?: string; -}; diff --git a/packages/twenty-apps/public/twenty-meeting-bot/src/logic-functions/types/files-field-value.type.ts b/packages/twenty-apps/public/twenty-meeting-bot/src/logic-functions/types/files-field-value.type.ts deleted file mode 100644 index e75a1bddd7c02..0000000000000 --- a/packages/twenty-apps/public/twenty-meeting-bot/src/logic-functions/types/files-field-value.type.ts +++ /dev/null @@ -1 +0,0 @@ -export type FilesFieldValue = { fileId: string }[]; diff --git a/packages/twenty-apps/public/twenty-meeting-bot/src/logic-functions/types/meeting-bot-policy-calendar-event-input.type.ts b/packages/twenty-apps/public/twenty-meeting-bot/src/logic-functions/types/meeting-bot-policy-calendar-event-input.type.ts deleted file mode 100644 index 0a87c8114a550..0000000000000 --- a/packages/twenty-apps/public/twenty-meeting-bot/src/logic-functions/types/meeting-bot-policy-calendar-event-input.type.ts +++ /dev/null @@ -1,10 +0,0 @@ -// Domain read shape: wire composites are flattened and absence is undefined. -export type MeetingBotPolicyCalendarEventInput = { - id: string; - isCanceled: boolean; - startsAt: string | undefined; - endsAt: string | undefined; - iCalUid: string | undefined; - conferenceLinkUrl: string | undefined; - meetingBotPreference: string | undefined; -}; diff --git a/packages/twenty-apps/public/twenty-meeting-bot/src/logic-functions/types/meeting-bot-policy-input.type.ts b/packages/twenty-apps/public/twenty-meeting-bot/src/logic-functions/types/meeting-bot-policy-input.type.ts deleted file mode 100644 index 5a90fd60f46b6..0000000000000 --- a/packages/twenty-apps/public/twenty-meeting-bot/src/logic-functions/types/meeting-bot-policy-input.type.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { type MeetingBotPreference } from 'src/constants/meeting-bot-preference'; - -export type MeetingBotPolicyInput = { - meetingBotPreference: MeetingBotPreference | undefined; - isCanceled: boolean; - startsAt: string | undefined; - endsAt: string | undefined; - conferenceLinkUrl: string | undefined; -}; diff --git a/packages/twenty-apps/public/twenty-meeting-bot/src/logic-functions/types/meeting-bot-policy-not-required-reason.type.ts b/packages/twenty-apps/public/twenty-meeting-bot/src/logic-functions/types/meeting-bot-policy-not-required-reason.type.ts deleted file mode 100644 index 748a5a5dc0f7e..0000000000000 --- a/packages/twenty-apps/public/twenty-meeting-bot/src/logic-functions/types/meeting-bot-policy-not-required-reason.type.ts +++ /dev/null @@ -1,5 +0,0 @@ -export type MeetingBotPolicyNotRequiredReason = - | 'EVENT_CANCELED' - | 'PREFERENCE_OFF' - | 'MISSING_CONFERENCE_LINK' - | 'EVENT_NOT_UPCOMING'; diff --git a/packages/twenty-apps/public/twenty-meeting-bot/src/logic-functions/types/meeting-bot-policy-required-reason.type.ts b/packages/twenty-apps/public/twenty-meeting-bot/src/logic-functions/types/meeting-bot-policy-required-reason.type.ts deleted file mode 100644 index ef80f8eaebf62..0000000000000 --- a/packages/twenty-apps/public/twenty-meeting-bot/src/logic-functions/types/meeting-bot-policy-required-reason.type.ts +++ /dev/null @@ -1 +0,0 @@ -export type MeetingBotPolicyRequiredReason = 'RECORDING_ENABLED'; diff --git a/packages/twenty-apps/public/twenty-meeting-bot/src/logic-functions/types/meeting-bot-policy-result-for-calendar-event.type.ts b/packages/twenty-apps/public/twenty-meeting-bot/src/logic-functions/types/meeting-bot-policy-result-for-calendar-event.type.ts deleted file mode 100644 index a8c5b0762ef8d..0000000000000 --- a/packages/twenty-apps/public/twenty-meeting-bot/src/logic-functions/types/meeting-bot-policy-result-for-calendar-event.type.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { type MeetingBotPreference } from 'src/constants/meeting-bot-preference'; -import { type MeetingBotPolicyResult } from 'src/logic-functions/types/meeting-bot-policy-result.type'; - -export type MeetingBotPolicyResultForCalendarEvent = MeetingBotPolicyResult & { - calendarEventId: string; - meetingBotPreference: MeetingBotPreference | undefined; - realMeetingKey: string; -}; diff --git a/packages/twenty-apps/public/twenty-meeting-bot/src/logic-functions/types/meeting-bot-policy-result-for-meeting.type.ts b/packages/twenty-apps/public/twenty-meeting-bot/src/logic-functions/types/meeting-bot-policy-result-for-meeting.type.ts deleted file mode 100644 index 7091c0addba49..0000000000000 --- a/packages/twenty-apps/public/twenty-meeting-bot/src/logic-functions/types/meeting-bot-policy-result-for-meeting.type.ts +++ /dev/null @@ -1,6 +0,0 @@ -export type MeetingBotPolicyResultForMeeting = { - realMeetingKey: string; - shouldRequestBot: boolean; - calendarEventIds: string[]; - requestingCalendarEventIds: string[]; -}; diff --git a/packages/twenty-apps/public/twenty-meeting-bot/src/logic-functions/types/meeting-bot-policy-result.type.ts b/packages/twenty-apps/public/twenty-meeting-bot/src/logic-functions/types/meeting-bot-policy-result.type.ts deleted file mode 100644 index 2b420ff94f7ea..0000000000000 --- a/packages/twenty-apps/public/twenty-meeting-bot/src/logic-functions/types/meeting-bot-policy-result.type.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { type MeetingBotPolicyNotRequiredReason } from 'src/logic-functions/types/meeting-bot-policy-not-required-reason.type'; -import { type MeetingBotPolicyRequiredReason } from 'src/logic-functions/types/meeting-bot-policy-required-reason.type'; - -export type MeetingBotPolicyResult = - | { - shouldRequestBot: true; - reason: MeetingBotPolicyRequiredReason; - } - | { - shouldRequestBot: false; - reason: MeetingBotPolicyNotRequiredReason; - }; diff --git a/packages/twenty-apps/public/twenty-meeting-bot/src/logic-functions/types/meeting-bot-reconciliation-result.type.ts b/packages/twenty-apps/public/twenty-meeting-bot/src/logic-functions/types/meeting-bot-reconciliation-result.type.ts deleted file mode 100644 index ae463e04b5e9a..0000000000000 --- a/packages/twenty-apps/public/twenty-meeting-bot/src/logic-functions/types/meeting-bot-reconciliation-result.type.ts +++ /dev/null @@ -1,16 +0,0 @@ -export type MeetingBotReconciliationResult = - | { - action: 'CREATED' | 'UPDATED' | 'CANCELED'; - realMeetingKey: string; - callRecordingId: string; - } - | { - action: 'SKIPPED'; - realMeetingKey: string; - callRecordingId: string | null; - } - | { - action: 'FAILED'; - realMeetingKey: string; - errorMessage: string; - }; diff --git a/packages/twenty-apps/public/twenty-meeting-bot/src/logic-functions/types/meeting-recording.type.ts b/packages/twenty-apps/public/twenty-meeting-bot/src/logic-functions/types/meeting-recording.type.ts deleted file mode 100644 index 4c672082f4c7a..0000000000000 --- a/packages/twenty-apps/public/twenty-meeting-bot/src/logic-functions/types/meeting-recording.type.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { type CalendarEventRecord } from 'src/logic-functions/types/calendar-event-record.type'; -import { type CallRecordingRecord } from 'src/logic-functions/types/call-recording-record.type'; - -export type MeetingRecording = { - callRecording: CallRecordingRecord; - calendarEvent: CalendarEventRecord; -}; diff --git a/packages/twenty-apps/public/twenty-meeting-bot/src/logic-functions/types/recall-bot-metadata.type.ts b/packages/twenty-apps/public/twenty-meeting-bot/src/logic-functions/types/recall-bot-metadata.type.ts deleted file mode 100644 index 196f08cf02d7e..0000000000000 --- a/packages/twenty-apps/public/twenty-meeting-bot/src/logic-functions/types/recall-bot-metadata.type.ts +++ /dev/null @@ -1,6 +0,0 @@ -export type RecallBotMetadata = { - twentyWorkspaceId: string; - twentyCallRecordingId: string; - twentyCalendarEventId: string; - twentyRealMeetingKey: string; -}; diff --git a/packages/twenty-apps/public/twenty-meeting-bot/src/logic-functions/types/recall-bot-operation-result.type.ts b/packages/twenty-apps/public/twenty-meeting-bot/src/logic-functions/types/recall-bot-operation-result.type.ts deleted file mode 100644 index 3279288413f5e..0000000000000 --- a/packages/twenty-apps/public/twenty-meeting-bot/src/logic-functions/types/recall-bot-operation-result.type.ts +++ /dev/null @@ -1,19 +0,0 @@ -export type RecallBotOperationFailure = { - ok: false; - // null = no HTTP response (network failure), distinct from any status code. - status: number | null; - errorMessage: string; -}; - -export type RecallBotScheduleResult = - | { - ok: true; - externalBotId: string; - } - | RecallBotOperationFailure; - -export type RecallBotRemovalResult = - | { - ok: true; - } - | RecallBotOperationFailure; diff --git a/packages/twenty-apps/public/twenty-meeting-bot/src/logic-functions/types/removed-meeting-bot-occurrence.type.ts b/packages/twenty-apps/public/twenty-meeting-bot/src/logic-functions/types/removed-meeting-bot-occurrence.type.ts deleted file mode 100644 index 506a3490d5247..0000000000000 --- a/packages/twenty-apps/public/twenty-meeting-bot/src/logic-functions/types/removed-meeting-bot-occurrence.type.ts +++ /dev/null @@ -1,6 +0,0 @@ -// An occurrence whose event was deleted/moved; key + start re-checks siblings. -export type RemovedMeetingBotOccurrence = { - calendarEventId: string; - realMeetingKey: string; - startsAt: string | undefined; -}; diff --git a/packages/twenty-apps/public/twenty-meeting-bot/src/logic-functions/types/transcript-marker.type.ts b/packages/twenty-apps/public/twenty-meeting-bot/src/logic-functions/types/transcript-marker.type.ts deleted file mode 100644 index 205ce51d1c318..0000000000000 --- a/packages/twenty-apps/public/twenty-meeting-bot/src/logic-functions/types/transcript-marker.type.ts +++ /dev/null @@ -1,6 +0,0 @@ -export type TranscriptMarker = { - recallTranscriptId: string | null; - status: 'PENDING' | 'FAILED'; - requestedAt?: string; - subCode?: string | null; -}; diff --git a/packages/twenty-apps/public/twenty-meeting-bot/src/logic-functions/utils/as-record.util.ts b/packages/twenty-apps/public/twenty-meeting-bot/src/logic-functions/utils/as-record.util.ts deleted file mode 100644 index 63e7cfcddd29b..0000000000000 --- a/packages/twenty-apps/public/twenty-meeting-bot/src/logic-functions/utils/as-record.util.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { isArray, isObject } from '@sniptt/guards'; - -export const asRecord = (value: unknown): Record | undefined => - isObject(value) && !isArray(value) - ? (value as Record) - : undefined; diff --git a/packages/twenty-apps/public/twenty-meeting-bot/src/logic-functions/utils/get-application-variable-value.util.ts b/packages/twenty-apps/public/twenty-meeting-bot/src/logic-functions/utils/get-application-variable-value.util.ts deleted file mode 100644 index 19a9c2c7cd20f..0000000000000 --- a/packages/twenty-apps/public/twenty-meeting-bot/src/logic-functions/utils/get-application-variable-value.util.ts +++ /dev/null @@ -1,3 +0,0 @@ -// Application and server variables are injected into process.env on every execution. -export const getApplicationVariableValue = (key: string): string | undefined => - process.env[key]; diff --git a/packages/twenty-apps/public/twenty-meeting-bot/src/logic-functions/utils/get-record-at-path.util.ts b/packages/twenty-apps/public/twenty-meeting-bot/src/logic-functions/utils/get-record-at-path.util.ts deleted file mode 100644 index 1ee635a334fcf..0000000000000 --- a/packages/twenty-apps/public/twenty-meeting-bot/src/logic-functions/utils/get-record-at-path.util.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { asRecord } from 'src/logic-functions/utils/as-record.util'; - -export const getRecordAtPath = ( - record: Record | undefined, - path: string[], -): unknown => - path.reduce( - (currentValue, pathPart) => asRecord(currentValue)?.[pathPart], - record, - ); diff --git a/packages/twenty-apps/public/twenty-meeting-bot/src/logic-functions/utils/get-string.util.ts b/packages/twenty-apps/public/twenty-meeting-bot/src/logic-functions/utils/get-string.util.ts deleted file mode 100644 index f222af01b1de1..0000000000000 --- a/packages/twenty-apps/public/twenty-meeting-bot/src/logic-functions/utils/get-string.util.ts +++ /dev/null @@ -1,4 +0,0 @@ -import { isNonEmptyString } from 'src/logic-functions/utils/is-non-empty-string.util'; - -export const getString = (value: unknown): string | undefined => - isNonEmptyString(value) ? value : undefined; diff --git a/packages/twenty-apps/public/twenty-meeting-bot/src/logic-functions/utils/get-unique-sorted-ids.util.ts b/packages/twenty-apps/public/twenty-meeting-bot/src/logic-functions/utils/get-unique-sorted-ids.util.ts deleted file mode 100644 index a296142c0d397..0000000000000 --- a/packages/twenty-apps/public/twenty-meeting-bot/src/logic-functions/utils/get-unique-sorted-ids.util.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { isString } from '@sniptt/guards'; - -export const getUniqueSortedIds = ( - ids: Array, -): string[] => - [...new Set(ids.filter(isString))].sort((firstId, secondId) => - firstId.localeCompare(secondId), - ); diff --git a/packages/twenty-apps/public/twenty-meeting-bot/src/logic-functions/utils/is-non-empty-string.util.ts b/packages/twenty-apps/public/twenty-meeting-bot/src/logic-functions/utils/is-non-empty-string.util.ts deleted file mode 100644 index 175943f38363d..0000000000000 --- a/packages/twenty-apps/public/twenty-meeting-bot/src/logic-functions/utils/is-non-empty-string.util.ts +++ /dev/null @@ -1,5 +0,0 @@ -import { isString } from '@sniptt/guards'; - -// Trimming variant of @sniptt/guards isNonEmptyString, for normalizing at read boundaries. -export const isNonEmptyString = (value: unknown): value is string => - isString(value) && value.trim() !== ''; diff --git a/packages/twenty-apps/public/twenty-meeting-bot/src/page-layouts/calendar-event-recording-tab.ts b/packages/twenty-apps/public/twenty-meeting-bot/src/page-layouts/calendar-event-recording-tab.ts deleted file mode 100644 index 90d63b714261f..0000000000000 --- a/packages/twenty-apps/public/twenty-meeting-bot/src/page-layouts/calendar-event-recording-tab.ts +++ /dev/null @@ -1,33 +0,0 @@ -import { - definePageLayoutTab, - PageLayoutTabLayoutMode, -} from 'twenty-sdk/define'; - -import { CALENDAR_EVENT_RECORD_PAGE_LAYOUT_UNIVERSAL_IDENTIFIER } from 'src/constants/calendar-event-record-page-layout-universal-identifier'; -import { CALENDAR_EVENT_RECORDING_FRONT_COMPONENT_UNIVERSAL_IDENTIFIER } from 'src/constants/calendar-event-recording-front-component-universal-identifier'; -import { CALENDAR_EVENT_RECORDING_PAGE_LAYOUT_TAB_UNIVERSAL_IDENTIFIER } from 'src/constants/calendar-event-recording-page-layout-tab-universal-identifier'; -import { CALENDAR_EVENT_RECORDING_PAGE_LAYOUT_WIDGET_UNIVERSAL_IDENTIFIER } from 'src/constants/calendar-event-recording-page-layout-widget-universal-identifier'; - -export default definePageLayoutTab({ - universalIdentifier: - CALENDAR_EVENT_RECORDING_PAGE_LAYOUT_TAB_UNIVERSAL_IDENTIFIER, - title: 'Call Recording', - position: 25, - icon: 'IconVideo', - layoutMode: PageLayoutTabLayoutMode.CANVAS, - pageLayoutUniversalIdentifier: - CALENDAR_EVENT_RECORD_PAGE_LAYOUT_UNIVERSAL_IDENTIFIER, - widgets: [ - { - universalIdentifier: - CALENDAR_EVENT_RECORDING_PAGE_LAYOUT_WIDGET_UNIVERSAL_IDENTIFIER, - title: 'Transcript', - type: 'FRONT_COMPONENT', - configuration: { - configurationType: 'FRONT_COMPONENT', - frontComponentUniversalIdentifier: - CALENDAR_EVENT_RECORDING_FRONT_COMPONENT_UNIVERSAL_IDENTIFIER, - }, - }, - ], -}); diff --git a/packages/twenty-apps/public/twenty-meeting-bot/src/view-fields/meeting-bot-preference-on-calendar-event.view-field.ts b/packages/twenty-apps/public/twenty-meeting-bot/src/view-fields/meeting-bot-preference-on-calendar-event.view-field.ts deleted file mode 100644 index 8cba6d1ae81fd..0000000000000 --- a/packages/twenty-apps/public/twenty-meeting-bot/src/view-fields/meeting-bot-preference-on-calendar-event.view-field.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { defineViewField } from 'twenty-sdk/define'; - -import { MEETING_BOT_PREFERENCE_ON_CALENDAR_EVENT_FIELD_UNIVERSAL_IDENTIFIER } from 'src/constants/meeting-bot-preference-on-calendar-event-field-universal-identifier'; -import { MEETING_BOT_PREFERENCE_ON_CALENDAR_EVENT_VIEW_FIELD_UNIVERSAL_IDENTIFIER } from 'src/constants/meeting-bot-preference-on-calendar-event-view-field-universal-identifier'; - -// TODO: hardcoded because the published twenty-sdk (2.14.0) predates the -// calendarEventRecordPageFields view. Replace both with -// STANDARD_OBJECT_UNIVERSAL_IDENTIFIERS.calendarEvent.views.calendarEventRecordPageFields -// once the SDK is republished with this view. -const CALENDAR_EVENT_RECORD_PAGE_FIELDS_VIEW_UNIVERSAL_IDENTIFIER = - 'c73668d1-022d-4eaf-b825-4e2548180db6'; -const CALENDAR_EVENT_RECORD_PAGE_FIELDS_GENERAL_GROUP_UNIVERSAL_IDENTIFIER = - 'aeadeb9e-3673-4c0c-8845-f59cb1e6ca42'; - -export default defineViewField({ - universalIdentifier: - MEETING_BOT_PREFERENCE_ON_CALENDAR_EVENT_VIEW_FIELD_UNIVERSAL_IDENTIFIER, - viewUniversalIdentifier: - CALENDAR_EVENT_RECORD_PAGE_FIELDS_VIEW_UNIVERSAL_IDENTIFIER, - viewFieldGroupUniversalIdentifier: - CALENDAR_EVENT_RECORD_PAGE_FIELDS_GENERAL_GROUP_UNIVERSAL_IDENTIFIER, - fieldMetadataUniversalIdentifier: - MEETING_BOT_PREFERENCE_ON_CALENDAR_EVENT_FIELD_UNIVERSAL_IDENTIFIER, - position: 8, - isVisible: true, - size: 150, -}); diff --git a/packages/twenty-apps/public/twenty-meeting-bot/tsconfig.json b/packages/twenty-apps/public/twenty-meeting-bot/tsconfig.json deleted file mode 100644 index f71645f97ac85..0000000000000 --- a/packages/twenty-apps/public/twenty-meeting-bot/tsconfig.json +++ /dev/null @@ -1,42 +0,0 @@ -{ - "compileOnSave": false, - "compilerOptions": { - "sourceMap": true, - "declaration": true, - "outDir": "./dist", - "rootDir": ".", - "jsx": "react-jsx", - "moduleResolution": "bundler", - "allowSyntheticDefaultImports": true, - "emitDecoratorMetadata": true, - "experimentalDecorators": true, - "importHelpers": true, - "allowUnreachableCode": false, - "strict": true, - "alwaysStrict": true, - "noImplicitAny": true, - "strictBindCallApply": false, - "target": "es2018", - "module": "esnext", - "lib": ["es2020", "dom"], - "skipLibCheck": true, - "skipDefaultLibCheck": true, - "resolveJsonModule": true, - "paths": { - "src/*": ["./src/*"], - "~/*": ["./*"] - } - }, - "exclude": [ - "node_modules", - "dist", - "**/*.test.ts", - "**/*.spec.ts", - "**/*.integration-test.ts" - ], - "references": [ - { - "path": "./tsconfig.spec.json" - } - ] -} diff --git a/packages/twenty-apps/public/twenty-meeting-bot/tsconfig.spec.json b/packages/twenty-apps/public/twenty-meeting-bot/tsconfig.spec.json deleted file mode 100644 index ea69a6c0105e7..0000000000000 --- a/packages/twenty-apps/public/twenty-meeting-bot/tsconfig.spec.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "extends": "./tsconfig.json", - "compilerOptions": { - "composite": true, - "types": ["vitest/globals", "node"] - }, - "include": ["src/**/*.ts", "src/**/*.tsx"], - "exclude": ["node_modules", "dist"] -} diff --git a/packages/twenty-apps/public/twenty-meeting-bot/vitest.config.ts b/packages/twenty-apps/public/twenty-meeting-bot/vitest.config.ts deleted file mode 100644 index 055af77dbec35..0000000000000 --- a/packages/twenty-apps/public/twenty-meeting-bot/vitest.config.ts +++ /dev/null @@ -1,31 +0,0 @@ -import tsconfigPaths from 'vite-tsconfig-paths'; -import { defineConfig } from 'vitest/config'; - -const TWENTY_API_URL = process.env.TWENTY_API_URL ?? 'http://localhost:2020'; -const TWENTY_API_KEY = - process.env.TWENTY_API_KEY ?? - 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIyMDIwMjAyMC0xYzI1LTRkMDItYmYyNS02YWVjY2Y3ZWE0MTkiLCJ0eXBlIjoiQVBJX0tFWSIsIndvcmtzcGFjZUlkIjoiMjAyMDIwMjAtMWMyNS00ZDAyLWJmMjUtNmFlY2NmN2VhNDE5IiwiaWF0IjoxNzM1Njg5NjAwLCJleHAiOjQ4OTE0NDk2MDAsImp0aSI6IjIwMjAyMDIwLWY0MDEtNGQ4YS1hNzMxLTY0ZDAwN2MyN2JhZCJ9.bfQjfyN0NEtTCLE_xPyNcwonDzlSXFoP8kdCQTdnuDc'; - -// Make env vars available to globalSetup (test.env only applies to workers) -process.env.TWENTY_API_URL = TWENTY_API_URL; -process.env.TWENTY_API_KEY = TWENTY_API_KEY; - -export default defineConfig({ - plugins: [ - tsconfigPaths({ - projects: ['tsconfig.spec.json'], - ignoreConfigErrors: true, - }), - ], - test: { - testTimeout: 120_000, - hookTimeout: 120_000, - fileParallelism: false, - include: ['src/**/*.integration-test.ts'], - globalSetup: ['src/__tests__/global-setup.ts'], - env: { - TWENTY_API_URL, - TWENTY_API_KEY, - }, - }, -}); diff --git a/packages/twenty-apps/public/twenty-meeting-bot/vitest.unit.config.ts b/packages/twenty-apps/public/twenty-meeting-bot/vitest.unit.config.ts deleted file mode 100644 index e0d140015bb71..0000000000000 --- a/packages/twenty-apps/public/twenty-meeting-bot/vitest.unit.config.ts +++ /dev/null @@ -1,14 +0,0 @@ -import tsconfigPaths from 'vite-tsconfig-paths'; -import { defineConfig } from 'vitest/config'; - -export default defineConfig({ - plugins: [ - tsconfigPaths({ - projects: ['tsconfig.spec.json'], - ignoreConfigErrors: true, - }), - ], - test: { - include: ['src/**/*.test.ts'], - }, -}); diff --git a/packages/twenty-apps/public/twenty-meeting-bot/yarn.lock b/packages/twenty-apps/public/twenty-meeting-bot/yarn.lock deleted file mode 100644 index 2282a5403f1ac..0000000000000 --- a/packages/twenty-apps/public/twenty-meeting-bot/yarn.lock +++ /dev/null @@ -1,3396 +0,0 @@ -# This file is generated by running "yarn install" inside your project. -# Manual changes might be lost - proceed with caution! - -__metadata: - version: 8 - cacheKey: 10c0 - -"@alcalzone/ansi-tokenize@npm:^0.2.4": - version: 0.2.5 - resolution: "@alcalzone/ansi-tokenize@npm:0.2.5" - dependencies: - ansi-styles: "npm:^6.2.1" - is-fullwidth-code-point: "npm:^5.0.0" - checksum: 10c0/dd8622288426b5b7dbf8b68d51d4b930cea591d0e3b9dbc3f523131464d78ac922c165fcb7ba5307f133d35dbcaa0e6648a1c3fb6a0dc2725546d6f867b70af4 - languageName: node - linkType: hard - -"@babel/code-frame@npm:^7.0.0, @babel/code-frame@npm:^7.29.7": - version: 7.29.7 - resolution: "@babel/code-frame@npm:7.29.7" - dependencies: - "@babel/helper-validator-identifier": "npm:^7.29.7" - js-tokens: "npm:^4.0.0" - picocolors: "npm:^1.1.1" - checksum: 10c0/169fc2080169a40c1760155eaaaf739bcb882df0bec76a83adbda5493645bc17270a3434b8848c494b1933e96fe1d147370001e3cda09a39f43ae30f08ef2069 - languageName: node - linkType: hard - -"@babel/generator@npm:^7.29.7": - version: 7.29.7 - resolution: "@babel/generator@npm:7.29.7" - dependencies: - "@babel/parser": "npm:^7.29.7" - "@babel/types": "npm:^7.29.7" - "@jridgewell/gen-mapping": "npm:^0.3.12" - "@jridgewell/trace-mapping": "npm:^0.3.28" - jsesc: "npm:^3.0.2" - checksum: 10c0/9bf72b01b5bd0ea5b1288a0e37dbd360bff2f2b1ce73342c0d40fb3db2ec3dc004ada5ffa925c5e12939a416eed59e600d562b8ecd938ce0d27dfd0eb6c6c2b7 - languageName: node - linkType: hard - -"@babel/helper-globals@npm:^7.29.7": - version: 7.29.7 - resolution: "@babel/helper-globals@npm:7.29.7" - checksum: 10c0/f38417c40b1129a1b2b519ca961b9040c8827d1444fd74068702286b91b77089431dc76b6b9d5c1496e5da2a4f3ad329c6946e688ba3fa0d1d0b3d2b4f34f36a - languageName: node - linkType: hard - -"@babel/helper-module-imports@npm:^7.16.7": - version: 7.29.7 - resolution: "@babel/helper-module-imports@npm:7.29.7" - dependencies: - "@babel/traverse": "npm:^7.29.7" - "@babel/types": "npm:^7.29.7" - checksum: 10c0/6adf60d97356027413342a092f818d9678c4f5caff716a33e3284b5ae14e47a9e88059d421dde4ee4894691260039a12602c0e7becadc175602194b40dfa345d - languageName: node - linkType: hard - -"@babel/helper-string-parser@npm:^7.29.7": - version: 7.29.7 - resolution: "@babel/helper-string-parser@npm:7.29.7" - checksum: 10c0/194bc0f1716e396d5ffde56ad6119745fb9557662c98611590e5e454906783a4ccb21ce93056b8eb69a4909044834e45d96e50ac695bbe9e3221648fe033c06c - languageName: node - linkType: hard - -"@babel/helper-validator-identifier@npm:^7.29.7": - version: 7.29.7 - resolution: "@babel/helper-validator-identifier@npm:7.29.7" - checksum: 10c0/4795354e7ae0dcafa72de1cd04ec51252dc1498517170beaf019e03effc5b7bf13c6b21a3949a77e07b8125be7f106ed1131350d8ebd4566ae874094a726d62b - languageName: node - linkType: hard - -"@babel/parser@npm:^7.29.7": - version: 7.29.7 - resolution: "@babel/parser@npm:7.29.7" - dependencies: - "@babel/types": "npm:^7.29.7" - bin: - parser: ./bin/babel-parser.js - checksum: 10c0/65133038f80b54a714d6027cb77cee3f9a6b5c4c6842ce674301e13947cbcbfa8055e63acaf1b84c085d34226a14425b2c2b97b829e0e226d2e8f1299942a51d - languageName: node - linkType: hard - -"@babel/runtime@npm:^7.12.5, @babel/runtime@npm:^7.18.3": - version: 7.29.7 - resolution: "@babel/runtime@npm:7.29.7" - checksum: 10c0/ca11572f7146b21e0bde6a9ed4bb6a89eafbee5f0944c7eb54d0d8a2dac962c33638a1d611e14faa71dfbb92b4b5f9236232208568a6b7d5c6f3f39ddb91771e - languageName: node - linkType: hard - -"@babel/template@npm:^7.29.7": - version: 7.29.7 - resolution: "@babel/template@npm:7.29.7" - dependencies: - "@babel/code-frame": "npm:^7.29.7" - "@babel/parser": "npm:^7.29.7" - "@babel/types": "npm:^7.29.7" - checksum: 10c0/8bb7f900dcab0e9e1c5ffbc33ca10e0d26b7b2e2ca804becb73ee771b9c4ed6e2908a4ae4a14c08560febb45d2b6b9a173955e42ad404d05f8b04840a14d9c58 - languageName: node - linkType: hard - -"@babel/traverse@npm:^7.29.7": - version: 7.29.7 - resolution: "@babel/traverse@npm:7.29.7" - dependencies: - "@babel/code-frame": "npm:^7.29.7" - "@babel/generator": "npm:^7.29.7" - "@babel/helper-globals": "npm:^7.29.7" - "@babel/parser": "npm:^7.29.7" - "@babel/template": "npm:^7.29.7" - "@babel/types": "npm:^7.29.7" - debug: "npm:^4.3.1" - checksum: 10c0/e256a1fbdb956555b76f3c285b1e453f6bedec8b3afb61751d99d933efd11c7d79caf5ddf2493570058a9f7deaa1b48324380d7c1aa1443fd9508becbf56331a - languageName: node - linkType: hard - -"@babel/types@npm:^7.29.7": - version: 7.29.7 - resolution: "@babel/types@npm:7.29.7" - dependencies: - "@babel/helper-string-parser": "npm:^7.29.7" - "@babel/helper-validator-identifier": "npm:^7.29.7" - checksum: 10c0/b6623994c69717fa27294f5fa46d59140338e2d86c6c1c13085c84ef7d53086ee357fbf4fe9abe3dd3da75734dc77c4c0df2f90fb29e667558bb3b3fb705e88f - languageName: node - linkType: hard - -"@emnapi/core@npm:1.10.0": - version: 1.10.0 - resolution: "@emnapi/core@npm:1.10.0" - dependencies: - "@emnapi/wasi-threads": "npm:1.2.1" - tslib: "npm:^2.4.0" - checksum: 10c0/f51d08227857b60632de7714d708124f0e100a1462dde6df8221760939aa3204a73193830371830fac0716f3ccd2129f2cac1b17cd7d7958bc4da9018a296edb - languageName: node - linkType: hard - -"@emnapi/runtime@npm:1.10.0": - version: 1.10.0 - resolution: "@emnapi/runtime@npm:1.10.0" - dependencies: - tslib: "npm:^2.4.0" - checksum: 10c0/953f14991d1aefb92ee6f8eb27dea725e484791a53a0cb5f47d9e0087b9a2c929ff2e92adf95af15d6ad456db6300c6b761ebf72b50a875b874a83520b3ba093 - languageName: node - linkType: hard - -"@emnapi/wasi-threads@npm:1.2.1": - version: 1.2.1 - resolution: "@emnapi/wasi-threads@npm:1.2.1" - dependencies: - tslib: "npm:^2.4.0" - checksum: 10c0/32fcfa81ab396533b2ec1f4082b1ff779a05d9c836bbbd3f4398405b0e6814c0d9503b7993130e37bc6941dbc1ded49f55e9700ae9ca4e803bab2b5bc5deb331 - languageName: node - linkType: hard - -"@emotion/babel-plugin@npm:^11.13.5": - version: 11.13.5 - resolution: "@emotion/babel-plugin@npm:11.13.5" - dependencies: - "@babel/helper-module-imports": "npm:^7.16.7" - "@babel/runtime": "npm:^7.18.3" - "@emotion/hash": "npm:^0.9.2" - "@emotion/memoize": "npm:^0.9.0" - "@emotion/serialize": "npm:^1.3.3" - babel-plugin-macros: "npm:^3.1.0" - convert-source-map: "npm:^1.5.0" - escape-string-regexp: "npm:^4.0.0" - find-root: "npm:^1.1.0" - source-map: "npm:^0.5.7" - stylis: "npm:4.2.0" - checksum: 10c0/8ccbfec7defd0e513cb8a1568fa179eac1e20c35fda18aed767f6c59ea7314363ebf2de3e9d2df66c8ad78928dc3dceeded84e6fa8059087cae5c280090aeeeb - languageName: node - linkType: hard - -"@emotion/cache@npm:^11.14.0": - version: 11.14.0 - resolution: "@emotion/cache@npm:11.14.0" - dependencies: - "@emotion/memoize": "npm:^0.9.0" - "@emotion/sheet": "npm:^1.4.0" - "@emotion/utils": "npm:^1.4.2" - "@emotion/weak-memoize": "npm:^0.4.0" - stylis: "npm:4.2.0" - checksum: 10c0/3fa3e7a431ab6f8a47c67132a00ac8358f428c1b6c8421d4b20de9df7c18e95eec04a5a6ff5a68908f98d3280044f247b4965ac63df8302d2c94dba718769724 - languageName: node - linkType: hard - -"@emotion/hash@npm:^0.9.2": - version: 0.9.2 - resolution: "@emotion/hash@npm:0.9.2" - checksum: 10c0/0dc254561a3cc0a06a10bbce7f6a997883fd240c8c1928b93713f803a2e9153a257a488537012efe89dbe1246f2abfe2add62cdb3471a13d67137fcb808e81c2 - languageName: node - linkType: hard - -"@emotion/is-prop-valid@npm:^1.3.0": - version: 1.4.0 - resolution: "@emotion/is-prop-valid@npm:1.4.0" - dependencies: - "@emotion/memoize": "npm:^0.9.0" - checksum: 10c0/5f857814ec7d8c7e727727346dfb001af6b1fb31d621a3ce9c3edf944a484d8b0d619546c30899ae3ade2f317c76390ba4394449728e9bf628312defc2c41ac3 - languageName: node - linkType: hard - -"@emotion/memoize@npm:^0.9.0": - version: 0.9.0 - resolution: "@emotion/memoize@npm:0.9.0" - checksum: 10c0/13f474a9201c7f88b543e6ea42f55c04fb2fdc05e6c5a3108aced2f7e7aa7eda7794c56bba02985a46d8aaa914fcdde238727a98341a96e2aec750d372dadd15 - languageName: node - linkType: hard - -"@emotion/react@npm:^11.14.0": - version: 11.14.0 - resolution: "@emotion/react@npm:11.14.0" - dependencies: - "@babel/runtime": "npm:^7.18.3" - "@emotion/babel-plugin": "npm:^11.13.5" - "@emotion/cache": "npm:^11.14.0" - "@emotion/serialize": "npm:^1.3.3" - "@emotion/use-insertion-effect-with-fallbacks": "npm:^1.2.0" - "@emotion/utils": "npm:^1.4.2" - "@emotion/weak-memoize": "npm:^0.4.0" - hoist-non-react-statics: "npm:^3.3.1" - peerDependencies: - react: ">=16.8.0" - peerDependenciesMeta: - "@types/react": - optional: true - checksum: 10c0/d0864f571a9f99ec643420ef31fde09e2006d3943a6aba079980e4d5f6e9f9fecbcc54b8f617fe003c00092ff9d5241179149ffff2810cb05cf72b4620cfc031 - languageName: node - linkType: hard - -"@emotion/serialize@npm:^1.3.3": - version: 1.3.3 - resolution: "@emotion/serialize@npm:1.3.3" - dependencies: - "@emotion/hash": "npm:^0.9.2" - "@emotion/memoize": "npm:^0.9.0" - "@emotion/unitless": "npm:^0.10.0" - "@emotion/utils": "npm:^1.4.2" - csstype: "npm:^3.0.2" - checksum: 10c0/b28cb7de59de382021de2b26c0c94ebbfb16967a1b969a56fdb6408465a8993df243bfbd66430badaa6800e1834724e84895f5a6a9d97d0d224de3d77852acb4 - languageName: node - linkType: hard - -"@emotion/sheet@npm:^1.4.0": - version: 1.4.0 - resolution: "@emotion/sheet@npm:1.4.0" - checksum: 10c0/3ca72d1650a07d2fbb7e382761b130b4a887dcd04e6574b2d51ce578791240150d7072a9bcb4161933abbcd1e38b243a6fb4464a7fe991d700c17aa66bb5acc7 - languageName: node - linkType: hard - -"@emotion/styled@npm:^11.14.0": - version: 11.14.1 - resolution: "@emotion/styled@npm:11.14.1" - dependencies: - "@babel/runtime": "npm:^7.18.3" - "@emotion/babel-plugin": "npm:^11.13.5" - "@emotion/is-prop-valid": "npm:^1.3.0" - "@emotion/serialize": "npm:^1.3.3" - "@emotion/use-insertion-effect-with-fallbacks": "npm:^1.2.0" - "@emotion/utils": "npm:^1.4.2" - peerDependencies: - "@emotion/react": ^11.0.0-rc.0 - react: ">=16.8.0" - peerDependenciesMeta: - "@types/react": - optional: true - checksum: 10c0/2bbf8451df49c967e41fbcf8111a7f6dafe6757f0cc113f2f6e287206c45ac1d54dc8a95a483b7c0cee8614b8a8d08155bded6453d6721de1f8cc8d5b9216963 - languageName: node - linkType: hard - -"@emotion/unitless@npm:^0.10.0": - version: 0.10.0 - resolution: "@emotion/unitless@npm:0.10.0" - checksum: 10c0/150943192727b7650eb9a6851a98034ddb58a8b6958b37546080f794696141c3760966ac695ab9af97efe10178690987aee4791f9f0ad1ff76783cdca83c1d49 - languageName: node - linkType: hard - -"@emotion/use-insertion-effect-with-fallbacks@npm:^1.2.0": - version: 1.2.0 - resolution: "@emotion/use-insertion-effect-with-fallbacks@npm:1.2.0" - peerDependencies: - react: ">=16.8.0" - checksum: 10c0/074dbc92b96bdc09209871070076e3b0351b6b47efefa849a7d9c37ab142130767609ca1831da0055988974e3b895c1de7606e4c421fecaa27c3e56a2afd3b08 - languageName: node - linkType: hard - -"@emotion/utils@npm:^1.4.2": - version: 1.4.2 - resolution: "@emotion/utils@npm:1.4.2" - checksum: 10c0/7d0010bf60a2a8c1a033b6431469de4c80e47aeb8fd856a17c1d1f76bbc3a03161a34aeaa78803566e29681ca551e7bf9994b68e9c5f5c796159923e44f78d9a - languageName: node - linkType: hard - -"@emotion/weak-memoize@npm:^0.4.0": - version: 0.4.0 - resolution: "@emotion/weak-memoize@npm:0.4.0" - checksum: 10c0/64376af11f1266042d03b3305c30b7502e6084868e33327e944b539091a472f089db307af69240f7188f8bc6b319276fd7b141a36613f1160d73d12a60f6ca1a - languageName: node - linkType: hard - -"@esbuild/aix-ppc64@npm:0.28.1": - version: 0.28.1 - resolution: "@esbuild/aix-ppc64@npm:0.28.1" - conditions: os=aix & cpu=ppc64 - languageName: node - linkType: hard - -"@esbuild/android-arm64@npm:0.28.1": - version: 0.28.1 - resolution: "@esbuild/android-arm64@npm:0.28.1" - conditions: os=android & cpu=arm64 - languageName: node - linkType: hard - -"@esbuild/android-arm@npm:0.28.1": - version: 0.28.1 - resolution: "@esbuild/android-arm@npm:0.28.1" - conditions: os=android & cpu=arm - languageName: node - linkType: hard - -"@esbuild/android-x64@npm:0.28.1": - version: 0.28.1 - resolution: "@esbuild/android-x64@npm:0.28.1" - conditions: os=android & cpu=x64 - languageName: node - linkType: hard - -"@esbuild/darwin-arm64@npm:0.28.1": - version: 0.28.1 - resolution: "@esbuild/darwin-arm64@npm:0.28.1" - conditions: os=darwin & cpu=arm64 - languageName: node - linkType: hard - -"@esbuild/darwin-x64@npm:0.28.1": - version: 0.28.1 - resolution: "@esbuild/darwin-x64@npm:0.28.1" - conditions: os=darwin & cpu=x64 - languageName: node - linkType: hard - -"@esbuild/freebsd-arm64@npm:0.28.1": - version: 0.28.1 - resolution: "@esbuild/freebsd-arm64@npm:0.28.1" - conditions: os=freebsd & cpu=arm64 - languageName: node - linkType: hard - -"@esbuild/freebsd-x64@npm:0.28.1": - version: 0.28.1 - resolution: "@esbuild/freebsd-x64@npm:0.28.1" - conditions: os=freebsd & cpu=x64 - languageName: node - linkType: hard - -"@esbuild/linux-arm64@npm:0.28.1": - version: 0.28.1 - resolution: "@esbuild/linux-arm64@npm:0.28.1" - conditions: os=linux & cpu=arm64 - languageName: node - linkType: hard - -"@esbuild/linux-arm@npm:0.28.1": - version: 0.28.1 - resolution: "@esbuild/linux-arm@npm:0.28.1" - conditions: os=linux & cpu=arm - languageName: node - linkType: hard - -"@esbuild/linux-ia32@npm:0.28.1": - version: 0.28.1 - resolution: "@esbuild/linux-ia32@npm:0.28.1" - conditions: os=linux & cpu=ia32 - languageName: node - linkType: hard - -"@esbuild/linux-loong64@npm:0.28.1": - version: 0.28.1 - resolution: "@esbuild/linux-loong64@npm:0.28.1" - conditions: os=linux & cpu=loong64 - languageName: node - linkType: hard - -"@esbuild/linux-mips64el@npm:0.28.1": - version: 0.28.1 - resolution: "@esbuild/linux-mips64el@npm:0.28.1" - conditions: os=linux & cpu=mips64el - languageName: node - linkType: hard - -"@esbuild/linux-ppc64@npm:0.28.1": - version: 0.28.1 - resolution: "@esbuild/linux-ppc64@npm:0.28.1" - conditions: os=linux & cpu=ppc64 - languageName: node - linkType: hard - -"@esbuild/linux-riscv64@npm:0.28.1": - version: 0.28.1 - resolution: "@esbuild/linux-riscv64@npm:0.28.1" - conditions: os=linux & cpu=riscv64 - languageName: node - linkType: hard - -"@esbuild/linux-s390x@npm:0.28.1": - version: 0.28.1 - resolution: "@esbuild/linux-s390x@npm:0.28.1" - conditions: os=linux & cpu=s390x - languageName: node - linkType: hard - -"@esbuild/linux-x64@npm:0.28.1": - version: 0.28.1 - resolution: "@esbuild/linux-x64@npm:0.28.1" - conditions: os=linux & cpu=x64 - languageName: node - linkType: hard - -"@esbuild/netbsd-arm64@npm:0.28.1": - version: 0.28.1 - resolution: "@esbuild/netbsd-arm64@npm:0.28.1" - conditions: os=netbsd & cpu=arm64 - languageName: node - linkType: hard - -"@esbuild/netbsd-x64@npm:0.28.1": - version: 0.28.1 - resolution: "@esbuild/netbsd-x64@npm:0.28.1" - conditions: os=netbsd & cpu=x64 - languageName: node - linkType: hard - -"@esbuild/openbsd-arm64@npm:0.28.1": - version: 0.28.1 - resolution: "@esbuild/openbsd-arm64@npm:0.28.1" - conditions: os=openbsd & cpu=arm64 - languageName: node - linkType: hard - -"@esbuild/openbsd-x64@npm:0.28.1": - version: 0.28.1 - resolution: "@esbuild/openbsd-x64@npm:0.28.1" - conditions: os=openbsd & cpu=x64 - languageName: node - linkType: hard - -"@esbuild/openharmony-arm64@npm:0.28.1": - version: 0.28.1 - resolution: "@esbuild/openharmony-arm64@npm:0.28.1" - conditions: os=openharmony & cpu=arm64 - languageName: node - linkType: hard - -"@esbuild/sunos-x64@npm:0.28.1": - version: 0.28.1 - resolution: "@esbuild/sunos-x64@npm:0.28.1" - conditions: os=sunos & cpu=x64 - languageName: node - linkType: hard - -"@esbuild/win32-arm64@npm:0.28.1": - version: 0.28.1 - resolution: "@esbuild/win32-arm64@npm:0.28.1" - conditions: os=win32 & cpu=arm64 - languageName: node - linkType: hard - -"@esbuild/win32-ia32@npm:0.28.1": - version: 0.28.1 - resolution: "@esbuild/win32-ia32@npm:0.28.1" - conditions: os=win32 & cpu=ia32 - languageName: node - linkType: hard - -"@esbuild/win32-x64@npm:0.28.1": - version: 0.28.1 - resolution: "@esbuild/win32-x64@npm:0.28.1" - conditions: os=win32 & cpu=x64 - languageName: node - linkType: hard - -"@genql/runtime@npm:^2.10.0": - version: 2.10.0 - resolution: "@genql/runtime@npm:2.10.0" - dependencies: - "@types/qs": "npm:^6.9.0" - "@types/ws": "npm:^6.0.1" - graphql-query-batcher: "npm:^1.0.1" - isomorphic-unfetch: "npm:^3.0.0" - lodash: "npm:^4.17.20" - subscriptions-transport-ws: "npm:^0.9.16" - tslib: "npm:^2.0.0" - utility-types: "npm:^3.10.0" - ws: "npm:^6.1.4" - zen-observable-ts: "npm:^0.8.21" - peerDependencies: - graphql: "*" - checksum: 10c0/e2a886c2469c933681e2b0ddd6a5b7f4cb12932251ba460e3cf2db4246817da79313ea4ba9769ec7cbe53ab9c1cb81ad8fcce6a969cd241185b79398d2a4f3c6 - languageName: node - linkType: hard - -"@inquirer/ansi@npm:^2.0.7": - version: 2.0.7 - resolution: "@inquirer/ansi@npm:2.0.7" - checksum: 10c0/a574f97a899f0d9346fa26b528b3f4a9ba6dcb9172288efb6b4314d8486470ed53d2f538200f66a25b843c6e0cbf83688c6d5174a8dc6eca853b291b09609c5a - languageName: node - linkType: hard - -"@inquirer/checkbox@npm:^5.2.1": - version: 5.2.1 - resolution: "@inquirer/checkbox@npm:5.2.1" - dependencies: - "@inquirer/ansi": "npm:^2.0.7" - "@inquirer/core": "npm:^11.2.1" - "@inquirer/figures": "npm:^2.0.7" - "@inquirer/type": "npm:^4.0.7" - peerDependencies: - "@types/node": ">=18" - peerDependenciesMeta: - "@types/node": - optional: true - checksum: 10c0/e3a845156c718fbbec228a0e7fd2a4f10775185615eac1f405b3a2bb2ae607dda6baf178fbd6487db0e12e5e33014536e81acefe65de57cd476a7aeaea6fb35d - languageName: node - linkType: hard - -"@inquirer/confirm@npm:^6.1.1": - version: 6.1.1 - resolution: "@inquirer/confirm@npm:6.1.1" - dependencies: - "@inquirer/core": "npm:^11.2.1" - "@inquirer/type": "npm:^4.0.7" - peerDependencies: - "@types/node": ">=18" - peerDependenciesMeta: - "@types/node": - optional: true - checksum: 10c0/4684406161c09327df830b4026f3165b31e13831276d215051586408ed434423263b15686393ce95a4b55058c1b7f9b08aa4b66f5ac930b47523fff75051d36f - languageName: node - linkType: hard - -"@inquirer/core@npm:^11.2.1": - version: 11.2.1 - resolution: "@inquirer/core@npm:11.2.1" - dependencies: - "@inquirer/ansi": "npm:^2.0.7" - "@inquirer/figures": "npm:^2.0.7" - "@inquirer/type": "npm:^4.0.7" - cli-width: "npm:^4.1.0" - fast-wrap-ansi: "npm:^0.2.0" - mute-stream: "npm:^3.0.0" - signal-exit: "npm:^4.1.0" - peerDependencies: - "@types/node": ">=18" - peerDependenciesMeta: - "@types/node": - optional: true - checksum: 10c0/b5be386cecd9e441ac2f9d3417a6ae1c4658b3ee6cdf5dae791211400f4de158851f81fca2245e2062833716f95366b9e1717770828cb7365e756c16e822f0d2 - languageName: node - linkType: hard - -"@inquirer/editor@npm:^5.2.2": - version: 5.2.2 - resolution: "@inquirer/editor@npm:5.2.2" - dependencies: - "@inquirer/core": "npm:^11.2.1" - "@inquirer/external-editor": "npm:^3.0.3" - "@inquirer/type": "npm:^4.0.7" - peerDependencies: - "@types/node": ">=18" - peerDependenciesMeta: - "@types/node": - optional: true - checksum: 10c0/a75e7012aad3ccc3ec84893463ed689924515a6cbaee8e2a475769fb27c12bcf359fcdd5f62e92107fc4be88fd69444c15adf13071982efc140c7015a6604d9a - languageName: node - linkType: hard - -"@inquirer/expand@npm:^5.1.1": - version: 5.1.1 - resolution: "@inquirer/expand@npm:5.1.1" - dependencies: - "@inquirer/core": "npm:^11.2.1" - "@inquirer/type": "npm:^4.0.7" - peerDependencies: - "@types/node": ">=18" - peerDependenciesMeta: - "@types/node": - optional: true - checksum: 10c0/4de661a042147759ce351971a4f92010ab66cb76450424e7d8aec5de79b449d14e8751f54f557d0b047180bca2b76707626f4835ee46603c6200139c31b59652 - languageName: node - linkType: hard - -"@inquirer/external-editor@npm:^3.0.3": - version: 3.0.3 - resolution: "@inquirer/external-editor@npm:3.0.3" - dependencies: - chardet: "npm:^2.1.1" - iconv-lite: "npm:^0.7.2" - peerDependencies: - "@types/node": ">=18" - peerDependenciesMeta: - "@types/node": - optional: true - checksum: 10c0/fc24ddbff15f0874db6498c16d2fe153bb19a670d83605f480486d6ff0ea872ae2f32a548fd555797989db09850fa019a6b5e49a8d40cfee0d7deacad17edc1e - languageName: node - linkType: hard - -"@inquirer/figures@npm:^2.0.7": - version: 2.0.7 - resolution: "@inquirer/figures@npm:2.0.7" - checksum: 10c0/e0573dc9ad25fa3628d5164745e52852d8cd832a9918605b7716df2e37a0005a0aaf40b6d81cef2ca09cb708b200e61b82d1dcd17003f572577e233c19a9ec7b - languageName: node - linkType: hard - -"@inquirer/input@npm:^5.1.2": - version: 5.1.2 - resolution: "@inquirer/input@npm:5.1.2" - dependencies: - "@inquirer/core": "npm:^11.2.1" - "@inquirer/type": "npm:^4.0.7" - peerDependencies: - "@types/node": ">=18" - peerDependenciesMeta: - "@types/node": - optional: true - checksum: 10c0/9c3614f8d79fc2bec07a088858a58967a162046c9292b0c6f0c6f63396cf391181cfb54bb636f5b3dce8d23924c24ee4a0310183a493c94190e4750218f3744d - languageName: node - linkType: hard - -"@inquirer/number@npm:^4.1.1": - version: 4.1.1 - resolution: "@inquirer/number@npm:4.1.1" - dependencies: - "@inquirer/core": "npm:^11.2.1" - "@inquirer/type": "npm:^4.0.7" - peerDependencies: - "@types/node": ">=18" - peerDependenciesMeta: - "@types/node": - optional: true - checksum: 10c0/06210dd70bf89d35242af43009b5e3f76a5f07d6275a9d842bbed61536032f5f349ecba1c20012975d7b0362deb89746d5903e90f21516da10bfd8c2055829a9 - languageName: node - linkType: hard - -"@inquirer/password@npm:^5.1.1": - version: 5.1.1 - resolution: "@inquirer/password@npm:5.1.1" - dependencies: - "@inquirer/ansi": "npm:^2.0.7" - "@inquirer/core": "npm:^11.2.1" - "@inquirer/type": "npm:^4.0.7" - peerDependencies: - "@types/node": ">=18" - peerDependenciesMeta: - "@types/node": - optional: true - checksum: 10c0/e4cb3a53b56743808f83958a8fe85738d721599690a4bc7640eaa07fb8f4765bc6f174e40c16efcc8a5958a7169667e240714a706a36e831f9c7a6822cbe8b55 - languageName: node - linkType: hard - -"@inquirer/prompts@npm:^8.5.2": - version: 8.5.2 - resolution: "@inquirer/prompts@npm:8.5.2" - dependencies: - "@inquirer/checkbox": "npm:^5.2.1" - "@inquirer/confirm": "npm:^6.1.1" - "@inquirer/editor": "npm:^5.2.2" - "@inquirer/expand": "npm:^5.1.1" - "@inquirer/input": "npm:^5.1.2" - "@inquirer/number": "npm:^4.1.1" - "@inquirer/password": "npm:^5.1.1" - "@inquirer/rawlist": "npm:^5.3.1" - "@inquirer/search": "npm:^4.2.1" - "@inquirer/select": "npm:^5.2.1" - peerDependencies: - "@types/node": ">=18" - peerDependenciesMeta: - "@types/node": - optional: true - checksum: 10c0/253b92e31c6a1f8f00a778eda8196bb53fc931723f7db4a03937f38ccd4d07c987766d4f60b9250b5d90bfe30b04747dfa73927a9f6fb886bd48091ccb202535 - languageName: node - linkType: hard - -"@inquirer/rawlist@npm:^5.3.1": - version: 5.3.1 - resolution: "@inquirer/rawlist@npm:5.3.1" - dependencies: - "@inquirer/core": "npm:^11.2.1" - "@inquirer/type": "npm:^4.0.7" - peerDependencies: - "@types/node": ">=18" - peerDependenciesMeta: - "@types/node": - optional: true - checksum: 10c0/a05eb6a16633bd7b32b3b6b2c1f645e6e28d955475b21ba7222e7e66806b1be15be9f91235b2933e8d27e04fdad81ebfb2d64fc1fc0d4b8d82dcd2718817b2b9 - languageName: node - linkType: hard - -"@inquirer/search@npm:^4.2.1": - version: 4.2.1 - resolution: "@inquirer/search@npm:4.2.1" - dependencies: - "@inquirer/core": "npm:^11.2.1" - "@inquirer/figures": "npm:^2.0.7" - "@inquirer/type": "npm:^4.0.7" - peerDependencies: - "@types/node": ">=18" - peerDependenciesMeta: - "@types/node": - optional: true - checksum: 10c0/eae9b2d524aae58266f96987696be22ad62f608661d28763c413f2560094d571a39d72a40630d2470f503ad5e6e50d8c574a653cc028bf79b51104fa91f59a67 - languageName: node - linkType: hard - -"@inquirer/select@npm:^5.2.1": - version: 5.2.1 - resolution: "@inquirer/select@npm:5.2.1" - dependencies: - "@inquirer/ansi": "npm:^2.0.7" - "@inquirer/core": "npm:^11.2.1" - "@inquirer/figures": "npm:^2.0.7" - "@inquirer/type": "npm:^4.0.7" - peerDependencies: - "@types/node": ">=18" - peerDependenciesMeta: - "@types/node": - optional: true - checksum: 10c0/3c6d0151b5a29111254a58eaf8b93bb4c476e68b0751526d8bff61a4bea457bdbe782890ae33977c5d2015f436596b5d32a8bb0677bfdf610f5f5998e65f5a92 - languageName: node - linkType: hard - -"@inquirer/type@npm:^4.0.7": - version: 4.0.7 - resolution: "@inquirer/type@npm:4.0.7" - peerDependencies: - "@types/node": ">=18" - peerDependenciesMeta: - "@types/node": - optional: true - checksum: 10c0/80678ac1c6e19ce309909e4a54a69adc95697ea3abc2cb92f17b1bc52f4caadbcb4003ae7339fb5a70c0d36d3bde975e1bb4450069662f41c953a0d28695bb70 - languageName: node - linkType: hard - -"@isaacs/fs-minipass@npm:^4.0.0": - version: 4.0.1 - resolution: "@isaacs/fs-minipass@npm:4.0.1" - dependencies: - minipass: "npm:^7.0.4" - checksum: 10c0/c25b6dc1598790d5b55c0947a9b7d111cfa92594db5296c3b907e2f533c033666f692a3939eadac17b1c7c40d362d0b0635dc874cbfe3e70db7c2b07cc97a5d2 - languageName: node - linkType: hard - -"@jridgewell/gen-mapping@npm:^0.3.12": - version: 0.3.13 - resolution: "@jridgewell/gen-mapping@npm:0.3.13" - dependencies: - "@jridgewell/sourcemap-codec": "npm:^1.5.0" - "@jridgewell/trace-mapping": "npm:^0.3.24" - checksum: 10c0/9a7d65fb13bd9aec1fbab74cda08496839b7e2ceb31f5ab922b323e94d7c481ce0fc4fd7e12e2610915ed8af51178bdc61e168e92a8c8b8303b030b03489b13b - languageName: node - linkType: hard - -"@jridgewell/resolve-uri@npm:^3.1.0": - version: 3.1.2 - resolution: "@jridgewell/resolve-uri@npm:3.1.2" - checksum: 10c0/d502e6fb516b35032331406d4e962c21fe77cdf1cbdb49c6142bcbd9e30507094b18972778a6e27cbad756209cfe34b1a27729e6fa08a2eb92b33943f680cf1e - languageName: node - linkType: hard - -"@jridgewell/sourcemap-codec@npm:^1.4.14, @jridgewell/sourcemap-codec@npm:^1.5.0, @jridgewell/sourcemap-codec@npm:^1.5.5": - version: 1.5.5 - resolution: "@jridgewell/sourcemap-codec@npm:1.5.5" - checksum: 10c0/f9e538f302b63c0ebc06eecb1dd9918dd4289ed36147a0ddce35d6ea4d7ebbda243cda7b2213b6a5e1d8087a298d5cf630fb2bd39329cdecb82017023f6081a0 - languageName: node - linkType: hard - -"@jridgewell/trace-mapping@npm:^0.3.24, @jridgewell/trace-mapping@npm:^0.3.28": - version: 0.3.31 - resolution: "@jridgewell/trace-mapping@npm:0.3.31" - dependencies: - "@jridgewell/resolve-uri": "npm:^3.1.0" - "@jridgewell/sourcemap-codec": "npm:^1.4.14" - checksum: 10c0/4b30ec8cd56c5fd9a661f088230af01e0c1a3888d11ffb6b47639700f71225be21d1f7e168048d6d4f9449207b978a235c07c8f15c07705685d16dc06280e9d9 - languageName: node - linkType: hard - -"@napi-rs/wasm-runtime@npm:^1.1.4": - version: 1.1.5 - resolution: "@napi-rs/wasm-runtime@npm:1.1.5" - dependencies: - "@tybys/wasm-util": "npm:^0.10.2" - peerDependencies: - "@emnapi/core": ^1.7.1 - "@emnapi/runtime": ^1.7.1 - checksum: 10c0/727f2b6ae0e68bbe5d39aeb68aa6f183314e9f03dc50bb55a962849535b2db53ecc3fbf1554d8656a54488a608df5a2634670595cf5874dc4af2ee59f817c65d - languageName: node - linkType: hard - -"@oxc-project/types@npm:=0.133.0": - version: 0.133.0 - resolution: "@oxc-project/types@npm:0.133.0" - checksum: 10c0/70c57ba58644f7ec217b670c301801f4d06995f4ccdba6b2bd106ea3e5ee49d616573e6ef8d55530b87571a960696543687f3850e87ad173d3f88965c30cdd63 - languageName: node - linkType: hard - -"@oxlint/darwin-arm64@npm:0.16.12": - version: 0.16.12 - resolution: "@oxlint/darwin-arm64@npm:0.16.12" - conditions: os=darwin & cpu=arm64 - languageName: node - linkType: hard - -"@oxlint/darwin-x64@npm:0.16.12": - version: 0.16.12 - resolution: "@oxlint/darwin-x64@npm:0.16.12" - conditions: os=darwin & cpu=x64 - languageName: node - linkType: hard - -"@oxlint/linux-arm64-gnu@npm:0.16.12": - version: 0.16.12 - resolution: "@oxlint/linux-arm64-gnu@npm:0.16.12" - conditions: os=linux & cpu=arm64 & libc=glibc - languageName: node - linkType: hard - -"@oxlint/linux-arm64-musl@npm:0.16.12": - version: 0.16.12 - resolution: "@oxlint/linux-arm64-musl@npm:0.16.12" - conditions: os=linux & cpu=arm64 & libc=musl - languageName: node - linkType: hard - -"@oxlint/linux-x64-gnu@npm:0.16.12": - version: 0.16.12 - resolution: "@oxlint/linux-x64-gnu@npm:0.16.12" - conditions: os=linux & cpu=x64 & libc=glibc - languageName: node - linkType: hard - -"@oxlint/linux-x64-musl@npm:0.16.12": - version: 0.16.12 - resolution: "@oxlint/linux-x64-musl@npm:0.16.12" - conditions: os=linux & cpu=x64 & libc=musl - languageName: node - linkType: hard - -"@oxlint/win32-arm64@npm:0.16.12": - version: 0.16.12 - resolution: "@oxlint/win32-arm64@npm:0.16.12" - conditions: os=win32 & cpu=arm64 - languageName: node - linkType: hard - -"@oxlint/win32-x64@npm:0.16.12": - version: 0.16.12 - resolution: "@oxlint/win32-x64@npm:0.16.12" - conditions: os=win32 & cpu=x64 - languageName: node - linkType: hard - -"@rolldown/binding-android-arm64@npm:1.0.3": - version: 1.0.3 - resolution: "@rolldown/binding-android-arm64@npm:1.0.3" - conditions: os=android & cpu=arm64 - languageName: node - linkType: hard - -"@rolldown/binding-darwin-arm64@npm:1.0.3": - version: 1.0.3 - resolution: "@rolldown/binding-darwin-arm64@npm:1.0.3" - conditions: os=darwin & cpu=arm64 - languageName: node - linkType: hard - -"@rolldown/binding-darwin-x64@npm:1.0.3": - version: 1.0.3 - resolution: "@rolldown/binding-darwin-x64@npm:1.0.3" - conditions: os=darwin & cpu=x64 - languageName: node - linkType: hard - -"@rolldown/binding-freebsd-x64@npm:1.0.3": - version: 1.0.3 - resolution: "@rolldown/binding-freebsd-x64@npm:1.0.3" - conditions: os=freebsd & cpu=x64 - languageName: node - linkType: hard - -"@rolldown/binding-linux-arm-gnueabihf@npm:1.0.3": - version: 1.0.3 - resolution: "@rolldown/binding-linux-arm-gnueabihf@npm:1.0.3" - conditions: os=linux & cpu=arm - languageName: node - linkType: hard - -"@rolldown/binding-linux-arm64-gnu@npm:1.0.3": - version: 1.0.3 - resolution: "@rolldown/binding-linux-arm64-gnu@npm:1.0.3" - conditions: os=linux & cpu=arm64 & libc=glibc - languageName: node - linkType: hard - -"@rolldown/binding-linux-arm64-musl@npm:1.0.3": - version: 1.0.3 - resolution: "@rolldown/binding-linux-arm64-musl@npm:1.0.3" - conditions: os=linux & cpu=arm64 & libc=musl - languageName: node - linkType: hard - -"@rolldown/binding-linux-ppc64-gnu@npm:1.0.3": - version: 1.0.3 - resolution: "@rolldown/binding-linux-ppc64-gnu@npm:1.0.3" - conditions: os=linux & cpu=ppc64 & libc=glibc - languageName: node - linkType: hard - -"@rolldown/binding-linux-s390x-gnu@npm:1.0.3": - version: 1.0.3 - resolution: "@rolldown/binding-linux-s390x-gnu@npm:1.0.3" - conditions: os=linux & cpu=s390x & libc=glibc - languageName: node - linkType: hard - -"@rolldown/binding-linux-x64-gnu@npm:1.0.3": - version: 1.0.3 - resolution: "@rolldown/binding-linux-x64-gnu@npm:1.0.3" - conditions: os=linux & cpu=x64 & libc=glibc - languageName: node - linkType: hard - -"@rolldown/binding-linux-x64-musl@npm:1.0.3": - version: 1.0.3 - resolution: "@rolldown/binding-linux-x64-musl@npm:1.0.3" - conditions: os=linux & cpu=x64 & libc=musl - languageName: node - linkType: hard - -"@rolldown/binding-openharmony-arm64@npm:1.0.3": - version: 1.0.3 - resolution: "@rolldown/binding-openharmony-arm64@npm:1.0.3" - conditions: os=openharmony & cpu=arm64 - languageName: node - linkType: hard - -"@rolldown/binding-wasm32-wasi@npm:1.0.3": - version: 1.0.3 - resolution: "@rolldown/binding-wasm32-wasi@npm:1.0.3" - dependencies: - "@emnapi/core": "npm:1.10.0" - "@emnapi/runtime": "npm:1.10.0" - "@napi-rs/wasm-runtime": "npm:^1.1.4" - conditions: cpu=wasm32 - languageName: node - linkType: hard - -"@rolldown/binding-win32-arm64-msvc@npm:1.0.3": - version: 1.0.3 - resolution: "@rolldown/binding-win32-arm64-msvc@npm:1.0.3" - conditions: os=win32 & cpu=arm64 - languageName: node - linkType: hard - -"@rolldown/binding-win32-x64-msvc@npm:1.0.3": - version: 1.0.3 - resolution: "@rolldown/binding-win32-x64-msvc@npm:1.0.3" - conditions: os=win32 & cpu=x64 - languageName: node - linkType: hard - -"@rolldown/pluginutils@npm:^1.0.0": - version: 1.0.1 - resolution: "@rolldown/pluginutils@npm:1.0.1" - checksum: 10c0/99d9b06d90196823e4d8c841f258db7a16e5dbba5824a2962b05d907b79f1ba929d56f22dd744fd530936e568c865ee56a719dc31e57e13bc0a8eb4764a8d8dd - languageName: node - linkType: hard - -"@sniptt/guards@npm:^0.2.0": - version: 0.2.0 - resolution: "@sniptt/guards@npm:0.2.0" - checksum: 10c0/749bb0f550d1ddd4abdb23dc1076cba26e977659922b73d000bceeb253c09270aaced0d18e92f09d4ce2fdaae33e55f60f2142f5359bc90ba505422af0873526 - languageName: node - linkType: hard - -"@standard-schema/spec@npm:^1.1.0": - version: 1.1.0 - resolution: "@standard-schema/spec@npm:1.1.0" - checksum: 10c0/d90f55acde4b2deb983529c87e8025fa693de1a5e8b49ecc6eb84d1fd96328add0e03d7d551442156c7432fd78165b2c26ff561b970a9a881f046abb78d6a526 - languageName: node - linkType: hard - -"@tybys/wasm-util@npm:^0.10.2": - version: 0.10.2 - resolution: "@tybys/wasm-util@npm:0.10.2" - dependencies: - tslib: "npm:^2.4.0" - checksum: 10c0/26165bcd1fd7269f42d7fbe3de318f854a8968de8397e89fc9a423bb3e2da35a52150f382e6323b3367595beb16d9800a6f35971a5599daf76da1742ec3afc25 - languageName: node - linkType: hard - -"@types/chai@npm:^5.2.2": - version: 5.2.3 - resolution: "@types/chai@npm:5.2.3" - dependencies: - "@types/deep-eql": "npm:*" - assertion-error: "npm:^2.0.1" - checksum: 10c0/e0ef1de3b6f8045a5e473e867c8565788c444271409d155588504840ad1a53611011f85072188c2833941189400228c1745d78323dac13fcede9c2b28bacfb2f - languageName: node - linkType: hard - -"@types/deep-eql@npm:*": - version: 4.0.2 - resolution: "@types/deep-eql@npm:4.0.2" - checksum: 10c0/bf3f811843117900d7084b9d0c852da9a044d12eb40e6de73b552598a6843c21291a8a381b0532644574beecd5e3491c5ff3a0365ab86b15d59862c025384844 - languageName: node - linkType: hard - -"@types/estree@npm:^1.0.0": - version: 1.0.9 - resolution: "@types/estree@npm:1.0.9" - checksum: 10c0/3ad3286ca2988cd550dafb8f2ad599c8474868e954fa601a36655bdfefd8039f7c714b8c1c7f2ae219ffbd58bd4660e66fa7479a0120fc02d4777057d4865387 - languageName: node - linkType: hard - -"@types/node@npm:*": - version: 25.9.3 - resolution: "@types/node@npm:25.9.3" - dependencies: - undici-types: "npm:>=7.24.0 <7.24.7" - checksum: 10c0/72d3aece9d42c2c641bcd3f3cb2dc2828b4bd384dfcbd910c404b8859a68bd69d50c4769ce7defd4ff5e049768e23e615f09407ea2cbbb5f44b90d75a7c6b8ca - languageName: node - linkType: hard - -"@types/node@npm:^24.7.2": - version: 24.13.2 - resolution: "@types/node@npm:24.13.2" - dependencies: - undici-types: "npm:~7.18.0" - checksum: 10c0/d7d48a88a4feb0a6aac3cbfaf9ef3b12752b4b09447f88dd0b4c77c03b281e3d4330fe6982a99aedcd63fc16c7540a0c248b91eb2abb0b3edd884d7fe684e9ea - languageName: node - linkType: hard - -"@types/parse-json@npm:^4.0.0": - version: 4.0.2 - resolution: "@types/parse-json@npm:4.0.2" - checksum: 10c0/b1b863ac34a2c2172fbe0807a1ec4d5cb684e48d422d15ec95980b81475fac4fdb3768a8b13eef39130203a7c04340fc167bae057c7ebcafd7dec9fe6c36aeb1 - languageName: node - linkType: hard - -"@types/qs@npm:^6.9.0": - version: 6.15.1 - resolution: "@types/qs@npm:6.15.1" - checksum: 10c0/1dfdbcb4cf2a8f66d57f0b9a9fe6b1c7091cb816687b6698c1351eaf31f62e412cea9b7453a9637b570cd5fad8dced527e5a9e69b4fcc6e318daacd8b749f094 - languageName: node - linkType: hard - -"@types/react@npm:^19.0.0": - version: 19.2.17 - resolution: "@types/react@npm:19.2.17" - dependencies: - csstype: "npm:^3.2.2" - checksum: 10c0/bc2c4af96b3e480604424de70d5ebda90c5f4b485df471858c0bc2d7d70364b606ec3c4d8579f94f01aa0c6c0591f56bcf14cba5689f5eea4b74250ccdc3a232 - languageName: node - linkType: hard - -"@types/ws@npm:^6.0.1": - version: 6.0.4 - resolution: "@types/ws@npm:6.0.4" - dependencies: - "@types/node": "npm:*" - checksum: 10c0/fa958e64596ca9487c3ed6012834de70b47f25d971f1950cfb8e6a99cb77ff340ae82ac7627744e01b58010674ef8ede07d5a2ac29ca9ad0d67a430fcc69ae14 - languageName: node - linkType: hard - -"@typescript/native-preview-darwin-arm64@npm:7.0.0-dev.20260619.1": - version: 7.0.0-dev.20260619.1 - resolution: "@typescript/native-preview-darwin-arm64@npm:7.0.0-dev.20260619.1" - conditions: os=darwin & cpu=arm64 - languageName: node - linkType: hard - -"@typescript/native-preview-darwin-x64@npm:7.0.0-dev.20260619.1": - version: 7.0.0-dev.20260619.1 - resolution: "@typescript/native-preview-darwin-x64@npm:7.0.0-dev.20260619.1" - conditions: os=darwin & cpu=x64 - languageName: node - linkType: hard - -"@typescript/native-preview-linux-arm64@npm:7.0.0-dev.20260619.1": - version: 7.0.0-dev.20260619.1 - resolution: "@typescript/native-preview-linux-arm64@npm:7.0.0-dev.20260619.1" - conditions: os=linux & cpu=arm64 - languageName: node - linkType: hard - -"@typescript/native-preview-linux-arm@npm:7.0.0-dev.20260619.1": - version: 7.0.0-dev.20260619.1 - resolution: "@typescript/native-preview-linux-arm@npm:7.0.0-dev.20260619.1" - conditions: os=linux & cpu=arm - languageName: node - linkType: hard - -"@typescript/native-preview-linux-x64@npm:7.0.0-dev.20260619.1": - version: 7.0.0-dev.20260619.1 - resolution: "@typescript/native-preview-linux-x64@npm:7.0.0-dev.20260619.1" - conditions: os=linux & cpu=x64 - languageName: node - linkType: hard - -"@typescript/native-preview-win32-arm64@npm:7.0.0-dev.20260619.1": - version: 7.0.0-dev.20260619.1 - resolution: "@typescript/native-preview-win32-arm64@npm:7.0.0-dev.20260619.1" - conditions: os=win32 & cpu=arm64 - languageName: node - linkType: hard - -"@typescript/native-preview-win32-x64@npm:7.0.0-dev.20260619.1": - version: 7.0.0-dev.20260619.1 - resolution: "@typescript/native-preview-win32-x64@npm:7.0.0-dev.20260619.1" - conditions: os=win32 & cpu=x64 - languageName: node - linkType: hard - -"@typescript/native-preview@npm:^7.0.0-dev.20260116.1": - version: 7.0.0-dev.20260619.1 - resolution: "@typescript/native-preview@npm:7.0.0-dev.20260619.1" - dependencies: - "@typescript/native-preview-darwin-arm64": "npm:7.0.0-dev.20260619.1" - "@typescript/native-preview-darwin-x64": "npm:7.0.0-dev.20260619.1" - "@typescript/native-preview-linux-arm": "npm:7.0.0-dev.20260619.1" - "@typescript/native-preview-linux-arm64": "npm:7.0.0-dev.20260619.1" - "@typescript/native-preview-linux-x64": "npm:7.0.0-dev.20260619.1" - "@typescript/native-preview-win32-arm64": "npm:7.0.0-dev.20260619.1" - "@typescript/native-preview-win32-x64": "npm:7.0.0-dev.20260619.1" - dependenciesMeta: - "@typescript/native-preview-darwin-arm64": - optional: true - "@typescript/native-preview-darwin-x64": - optional: true - "@typescript/native-preview-linux-arm": - optional: true - "@typescript/native-preview-linux-arm64": - optional: true - "@typescript/native-preview-linux-x64": - optional: true - "@typescript/native-preview-win32-arm64": - optional: true - "@typescript/native-preview-win32-x64": - optional: true - bin: - tsgo: bin/tsgo.js - checksum: 10c0/e8bc6cf171ee29131934f662c9b3577374d7e872247831da36a5723abe175998f1318a134ed8eb0ee5213887dbb1f1209fac5555cdf5b6b19d0d7da044d0cdc9 - languageName: node - linkType: hard - -"@vitest/expect@npm:4.1.9": - version: 4.1.9 - resolution: "@vitest/expect@npm:4.1.9" - dependencies: - "@standard-schema/spec": "npm:^1.1.0" - "@types/chai": "npm:^5.2.2" - "@vitest/spy": "npm:4.1.9" - "@vitest/utils": "npm:4.1.9" - chai: "npm:^6.2.2" - tinyrainbow: "npm:^3.1.0" - checksum: 10c0/243bacaed2cba5e0ea4ec7465662fcec465a358a0e06381e337fac49426aa67a73b104fbb9d65d8bccadfba8f70e27f57ffb897aacfa140f579a556367357875 - languageName: node - linkType: hard - -"@vitest/mocker@npm:4.1.9": - version: 4.1.9 - resolution: "@vitest/mocker@npm:4.1.9" - dependencies: - "@vitest/spy": "npm:4.1.9" - estree-walker: "npm:^3.0.3" - magic-string: "npm:^0.30.21" - peerDependencies: - msw: ^2.4.9 - vite: ^6.0.0 || ^7.0.0 || ^8.0.0 - peerDependenciesMeta: - msw: - optional: true - vite: - optional: true - checksum: 10c0/707353b7435bbfd441cc754e4ee7bc5921b70d07b051c6e414b6bbe4ca369154702b0ddeb603389469fe87ca1983e002eb2d55044582661f54a1945dd27e5c82 - languageName: node - linkType: hard - -"@vitest/pretty-format@npm:4.1.9": - version: 4.1.9 - resolution: "@vitest/pretty-format@npm:4.1.9" - dependencies: - tinyrainbow: "npm:^3.1.0" - checksum: 10c0/5b96295f25ab885616230ad1355fc82f490bebb39cc707688d7c8969c08270d7e076ed8a10af4e762ed57145193c6061a1f549f136f0ded344f8db0c2b3fb3de - languageName: node - linkType: hard - -"@vitest/runner@npm:4.1.9": - version: 4.1.9 - resolution: "@vitest/runner@npm:4.1.9" - dependencies: - "@vitest/utils": "npm:4.1.9" - pathe: "npm:^2.0.3" - checksum: 10c0/d206b4891a64b1f55c346f832b0a7b489108094d8ae34438d3b53e78be7b45b139fa95ffa027c98c357bd532268ee573168de1943235b7eed32a9236ed5978bb - languageName: node - linkType: hard - -"@vitest/snapshot@npm:4.1.9": - version: 4.1.9 - resolution: "@vitest/snapshot@npm:4.1.9" - dependencies: - "@vitest/pretty-format": "npm:4.1.9" - "@vitest/utils": "npm:4.1.9" - magic-string: "npm:^0.30.21" - pathe: "npm:^2.0.3" - checksum: 10c0/c3099df12ad1f9c1e180441856c9eb82f1990f87ff16aafedd6fa19978eaff20bc59220b692a99fcc822daef86eab256ba3dadb49544b7bd625b57c49cd9d995 - languageName: node - linkType: hard - -"@vitest/spy@npm:4.1.9": - version: 4.1.9 - resolution: "@vitest/spy@npm:4.1.9" - checksum: 10c0/e51f328f55b76e8ba66e5e18f183484a8dc0a092685b101112d3e9fb8e989ddca162c98ddf00254476502c25bc05c4ec1e277fd6ad8bfc702464c08f6b5dd115 - languageName: node - linkType: hard - -"@vitest/utils@npm:4.1.9": - version: 4.1.9 - resolution: "@vitest/utils@npm:4.1.9" - dependencies: - "@vitest/pretty-format": "npm:4.1.9" - convert-source-map: "npm:^2.0.0" - tinyrainbow: "npm:^3.1.0" - checksum: 10c0/d55506c077fd72c091eb66f02926f0abf72801c87a085f565698289562f47befa114ae2c680ab8736dfe46abab0cfd6b8031f2ac519bafeb37578aa6e5ad03c5 - languageName: node - linkType: hard - -"abbrev@npm:^4.0.0": - version: 4.0.0 - resolution: "abbrev@npm:4.0.0" - checksum: 10c0/b4cc16935235e80702fc90192e349e32f8ef0ed151ef506aa78c81a7c455ec18375c4125414b99f84b2e055199d66383e787675f0bcd87da7a4dbd59f9eac1d5 - languageName: node - linkType: hard - -"agent-base@npm:6": - version: 6.0.2 - resolution: "agent-base@npm:6.0.2" - dependencies: - debug: "npm:4" - checksum: 10c0/dc4f757e40b5f3e3d674bc9beb4f1048f4ee83af189bae39be99f57bf1f48dde166a8b0a5342a84b5944ee8e6ed1e5a9d801858f4ad44764e84957122fe46261 - languageName: node - linkType: hard - -"ansi-escapes@npm:^7.3.0": - version: 7.3.0 - resolution: "ansi-escapes@npm:7.3.0" - dependencies: - environment: "npm:^1.0.0" - checksum: 10c0/068961d99f0ef28b661a4a9f84a5d645df93ccf3b9b93816cc7d46bbe1913321d4cdf156bb842a4e1e4583b7375c631fa963efb43001c4eb7ff9ab8f78fc0679 - languageName: node - linkType: hard - -"ansi-regex@npm:^6.2.2": - version: 6.2.2 - resolution: "ansi-regex@npm:6.2.2" - checksum: 10c0/05d4acb1d2f59ab2cf4b794339c7b168890d44dda4bf0ce01152a8da0213aca207802f930442ce8cd22d7a92f44907664aac6508904e75e038fa944d2601b30f - languageName: node - linkType: hard - -"ansi-styles@npm:^6.2.1, ansi-styles@npm:^6.2.3": - version: 6.2.3 - resolution: "ansi-styles@npm:6.2.3" - checksum: 10c0/23b8a4ce14e18fb854693b95351e286b771d23d8844057ed2e7d083cd3e708376c3323707ec6a24365f7d7eda3ca00327fe04092e29e551499ec4c8b7bfac868 - languageName: node - linkType: hard - -"assertion-error@npm:^2.0.1": - version: 2.0.1 - resolution: "assertion-error@npm:2.0.1" - checksum: 10c0/bbbcb117ac6480138f8c93cf7f535614282dea9dc828f540cdece85e3c665e8f78958b96afac52f29ff883c72638e6a87d469ecc9fe5bc902df03ed24a55dba8 - languageName: node - linkType: hard - -"async-function@npm:^1.0.0": - version: 1.0.0 - resolution: "async-function@npm:1.0.0" - checksum: 10c0/669a32c2cb7e45091330c680e92eaeb791bc1d4132d827591e499cd1f776ff5a873e77e5f92d0ce795a8d60f10761dec9ddfe7225a5de680f5d357f67b1aac73 - languageName: node - linkType: hard - -"async-generator-function@npm:^1.0.0": - version: 1.0.0 - resolution: "async-generator-function@npm:1.0.0" - checksum: 10c0/2c50ef856c543ad500d8d8777d347e3c1ba623b93e99c9263ecc5f965c1b12d2a140e2ab6e43c3d0b85366110696f28114649411cbcd10b452a92a2318394186 - languageName: node - linkType: hard - -"async-limiter@npm:~1.0.0": - version: 1.0.1 - resolution: "async-limiter@npm:1.0.1" - checksum: 10c0/0693d378cfe86842a70d4c849595a0bb50dc44c11649640ca982fa90cbfc74e3cc4753b5a0847e51933f2e9c65ce8e05576e75e5e1fd963a086e673735b35969 - languageName: node - linkType: hard - -"asynckit@npm:^0.4.0": - version: 0.4.0 - resolution: "asynckit@npm:0.4.0" - checksum: 10c0/d73e2ddf20c4eb9337e1b3df1a0f6159481050a5de457c55b14ea2e5cb6d90bb69e004c9af54737a5ee0917fcf2c9e25de67777bbe58261847846066ba75bc9d - languageName: node - linkType: hard - -"auto-bind@npm:^5.0.1": - version: 5.0.1 - resolution: "auto-bind@npm:5.0.1" - checksum: 10c0/a703375350ea7b6e92405d8e6bcc6dbfb84b0d7c7172b33e5788a7593929a18227999ff9aa9c32436741d06d021e6672457b1cec73287efe3fab95cff6627eaf - languageName: node - linkType: hard - -"axios@npm:^1.16.0": - version: 1.17.0 - resolution: "axios@npm:1.17.0" - dependencies: - follow-redirects: "npm:^1.16.0" - form-data: "npm:^4.0.5" - https-proxy-agent: "npm:^5.0.1" - proxy-from-env: "npm:^2.1.0" - checksum: 10c0/c4fa19ff3a3a63bde48beec03ad816b133b9a6385cccffffe172577ab18c6a70e299280d57f12c80c867fe25df41f92cb91d3a8258708a6d2be3e9e085f92650 - languageName: node - linkType: hard - -"babel-plugin-macros@npm:^3.1.0": - version: 3.1.0 - resolution: "babel-plugin-macros@npm:3.1.0" - dependencies: - "@babel/runtime": "npm:^7.12.5" - cosmiconfig: "npm:^7.0.0" - resolve: "npm:^1.19.0" - checksum: 10c0/c6dfb15de96f67871d95bd2e8c58b0c81edc08b9b087dc16755e7157f357dc1090a8dc60ebab955e92587a9101f02eba07e730adc253a1e4cf593ca3ebd3839c - languageName: node - linkType: hard - -"backo2@npm:^1.0.2": - version: 1.0.2 - resolution: "backo2@npm:1.0.2" - checksum: 10c0/a9e825a6a38a6d1c4a94476eabc13d6127dfaafb0967baf104affbb67806ae26abbb58dab8d572d2cd21ef06634ff57c3ad48dff14b904e18de1474cc2f22bf3 - languageName: node - linkType: hard - -"call-bind-apply-helpers@npm:^1.0.1, call-bind-apply-helpers@npm:^1.0.2": - version: 1.0.2 - resolution: "call-bind-apply-helpers@npm:1.0.2" - dependencies: - es-errors: "npm:^1.3.0" - function-bind: "npm:^1.1.2" - checksum: 10c0/47bd9901d57b857590431243fea704ff18078b16890a6b3e021e12d279bbf211d039155e27d7566b374d49ee1f8189344bac9833dec7a20cdec370506361c938 - languageName: node - linkType: hard - -"callsites@npm:^3.0.0": - version: 3.1.0 - resolution: "callsites@npm:3.1.0" - checksum: 10c0/fff92277400eb06c3079f9e74f3af120db9f8ea03bad0e84d9aede54bbe2d44a56cccb5f6cf12211f93f52306df87077ecec5b712794c5a9b5dac6d615a3f301 - languageName: node - linkType: hard - -"chai@npm:^6.2.2": - version: 6.2.2 - resolution: "chai@npm:6.2.2" - checksum: 10c0/e6c69e5f0c11dffe6ea13d0290936ebb68fcc1ad688b8e952e131df6a6d5797d5e860bc55cef1aca2e950c3e1f96daf79e9d5a70fb7dbaab4e46355e2635ed53 - languageName: node - linkType: hard - -"chalk@npm:^5.3.0, chalk@npm:^5.6.0": - version: 5.6.2 - resolution: "chalk@npm:5.6.2" - checksum: 10c0/99a4b0f0e7991796b1e7e3f52dceb9137cae2a9dfc8fc0784a550dc4c558e15ab32ed70b14b21b52beb2679b4892b41a0aa44249bcb996f01e125d58477c6976 - languageName: node - linkType: hard - -"chardet@npm:^2.1.1": - version: 2.1.1 - resolution: "chardet@npm:2.1.1" - checksum: 10c0/d8391dd412338442b3de0d3a488aa9327f8bcf74b62b8723d6bd0b85c4084d50b731320e0a7c710edb1d44de75969995d2784b80e4c13b004a6c7a0db4c6e793 - languageName: node - linkType: hard - -"chokidar@npm:^4.0.0": - version: 4.0.3 - resolution: "chokidar@npm:4.0.3" - dependencies: - readdirp: "npm:^4.0.1" - checksum: 10c0/a58b9df05bb452f7d105d9e7229ac82fa873741c0c40ddcc7bb82f8a909fbe3f7814c9ebe9bc9a2bef9b737c0ec6e2d699d179048ef06ad3ec46315df0ebe6ad - languageName: node - linkType: hard - -"chownr@npm:^3.0.0": - version: 3.0.0 - resolution: "chownr@npm:3.0.0" - checksum: 10c0/43925b87700f7e3893296c8e9c56cc58f926411cce3a6e5898136daaf08f08b9a8eb76d37d3267e707d0dcc17aed2e2ebdf5848c0c3ce95cf910a919935c1b10 - languageName: node - linkType: hard - -"cli-boxes@npm:^3.0.0": - version: 3.0.0 - resolution: "cli-boxes@npm:3.0.0" - checksum: 10c0/4db3e8fbfaf1aac4fb3a6cbe5a2d3fa048bee741a45371b906439b9ffc821c6e626b0f108bdcd3ddf126a4a319409aedcf39a0730573ff050fdd7b6731e99fb9 - languageName: node - linkType: hard - -"cli-cursor@npm:^4.0.0": - version: 4.0.0 - resolution: "cli-cursor@npm:4.0.0" - dependencies: - restore-cursor: "npm:^4.0.0" - checksum: 10c0/e776e8c3c6727300d0539b0d25160b2bb56aed1a63942753ba1826b012f337a6f4b7ace3548402e4f2f13b5e16bfd751be672c44b203205e7eca8be94afec42c - languageName: node - linkType: hard - -"cli-truncate@npm:^5.1.1": - version: 5.2.0 - resolution: "cli-truncate@npm:5.2.0" - dependencies: - slice-ansi: "npm:^8.0.0" - string-width: "npm:^8.2.0" - checksum: 10c0/0d4ec94702ca85b64522ac93633837fb5ea7db17b79b1322a60f6045e6ae2b8cd7bd4c1d19ac7d1f9e10e3bbda1112e172e439b68c02b785ee00da8d6a5c5471 - languageName: node - linkType: hard - -"cli-width@npm:^4.1.0": - version: 4.1.0 - resolution: "cli-width@npm:4.1.0" - checksum: 10c0/1fbd56413578f6117abcaf858903ba1f4ad78370a4032f916745fa2c7e390183a9d9029cf837df320b0fdce8137668e522f60a30a5f3d6529ff3872d265a955f - languageName: node - linkType: hard - -"code-excerpt@npm:^4.0.0": - version: 4.0.0 - resolution: "code-excerpt@npm:4.0.0" - dependencies: - convert-to-spaces: "npm:^2.0.1" - checksum: 10c0/b6c5a06e039cecd2ab6a0e10ee0831de8362107d1f298ca3558b5f9004cb8e0260b02dd6c07f57b9a0e346c76864d2873311ee1989809fdeb05bd5fbbadde773 - languageName: node - linkType: hard - -"combined-stream@npm:^1.0.8": - version: 1.0.8 - resolution: "combined-stream@npm:1.0.8" - dependencies: - delayed-stream: "npm:~1.0.0" - checksum: 10c0/0dbb829577e1b1e839fa82b40c07ffaf7de8a09b935cadd355a73652ae70a88b4320db322f6634a4ad93424292fa80973ac6480986247f1734a1137debf271d5 - languageName: node - linkType: hard - -"commander@npm:^12.0.0": - version: 12.1.0 - resolution: "commander@npm:12.1.0" - checksum: 10c0/6e1996680c083b3b897bfc1cfe1c58dfbcd9842fd43e1aaf8a795fbc237f65efcc860a3ef457b318e73f29a4f4a28f6403c3d653d021d960e4632dd45bde54a9 - languageName: node - linkType: hard - -"convert-source-map@npm:^1.5.0": - version: 1.9.0 - resolution: "convert-source-map@npm:1.9.0" - checksum: 10c0/281da55454bf8126cbc6625385928c43479f2060984180c42f3a86c8b8c12720a24eac260624a7d1e090004028d2dee78602330578ceec1a08e27cb8bb0a8a5b - languageName: node - linkType: hard - -"convert-source-map@npm:^2.0.0": - version: 2.0.0 - resolution: "convert-source-map@npm:2.0.0" - checksum: 10c0/8f2f7a27a1a011cc6cc88cc4da2d7d0cfa5ee0369508baae3d98c260bb3ac520691464e5bbe4ae7cdf09860c1d69ecc6f70c63c6e7c7f7e3f18ec08484dc7d9b - languageName: node - linkType: hard - -"convert-to-spaces@npm:^2.0.1": - version: 2.0.1 - resolution: "convert-to-spaces@npm:2.0.1" - checksum: 10c0/d90aa0e3b6a27f9d5265a8d32def3c5c855b3e823a9db1f26d772f8146d6b91020a2fdfd905ce8048a73fad3aaf836fef8188c67602c374405e2ae8396c4ac46 - languageName: node - linkType: hard - -"cosmiconfig@npm:^7.0.0": - version: 7.1.0 - resolution: "cosmiconfig@npm:7.1.0" - dependencies: - "@types/parse-json": "npm:^4.0.0" - import-fresh: "npm:^3.2.1" - parse-json: "npm:^5.0.0" - path-type: "npm:^4.0.0" - yaml: "npm:^1.10.0" - checksum: 10c0/b923ff6af581638128e5f074a5450ba12c0300b71302398ea38dbeabd33bbcaa0245ca9adbedfcf284a07da50f99ede5658c80bb3e39e2ce770a99d28a21ef03 - languageName: node - linkType: hard - -"csstype@npm:^3.0.2, csstype@npm:^3.2.2": - version: 3.2.3 - resolution: "csstype@npm:3.2.3" - checksum: 10c0/cd29c51e70fa822f1cecd8641a1445bed7063697469d35633b516e60fe8c1bde04b08f6c5b6022136bb669b64c63d4173af54864510fbb4ee23281801841a3ce - languageName: node - linkType: hard - -"debug@npm:4, debug@npm:^4.1.1, debug@npm:^4.3.1": - version: 4.4.3 - resolution: "debug@npm:4.4.3" - dependencies: - ms: "npm:^2.1.3" - peerDependenciesMeta: - supports-color: - optional: true - checksum: 10c0/d79136ec6c83ecbefd0f6a5593da6a9c91ec4d7ddc4b54c883d6e71ec9accb5f67a1a5e96d00a328196b5b5c86d365e98d8a3a70856aaf16b4e7b1985e67f5a6 - languageName: node - linkType: hard - -"delayed-stream@npm:~1.0.0": - version: 1.0.0 - resolution: "delayed-stream@npm:1.0.0" - checksum: 10c0/d758899da03392e6712f042bec80aa293bbe9e9ff1b2634baae6a360113e708b91326594c8a486d475c69d6259afb7efacdc3537bfcda1c6c648e390ce601b19 - languageName: node - linkType: hard - -"detect-libc@npm:^2.0.3": - version: 2.1.2 - resolution: "detect-libc@npm:2.1.2" - checksum: 10c0/acc675c29a5649fa1fb6e255f993b8ee829e510b6b56b0910666949c80c364738833417d0edb5f90e4e46be17228b0f2b66a010513984e18b15deeeac49369c4 - languageName: node - linkType: hard - -"dunder-proto@npm:^1.0.1": - version: 1.0.1 - resolution: "dunder-proto@npm:1.0.1" - dependencies: - call-bind-apply-helpers: "npm:^1.0.1" - es-errors: "npm:^1.3.0" - gopd: "npm:^1.2.0" - checksum: 10c0/199f2a0c1c16593ca0a145dbf76a962f8033ce3129f01284d48c45ed4e14fea9bbacd7b3610b6cdc33486cef20385ac054948fefc6272fcce645c09468f93031 - languageName: node - linkType: hard - -"emoji-regex@npm:^10.3.0": - version: 10.6.0 - resolution: "emoji-regex@npm:10.6.0" - checksum: 10c0/1e4aa097bb007301c3b4b1913879ae27327fdc48e93eeefefe3b87e495eb33c5af155300be951b4349ff6ac084f4403dc9eff970acba7c1c572d89396a9a32d7 - languageName: node - linkType: hard - -"env-paths@npm:^2.2.0": - version: 2.2.1 - resolution: "env-paths@npm:2.2.1" - checksum: 10c0/285325677bf00e30845e330eec32894f5105529db97496ee3f598478e50f008c5352a41a30e5e72ec9de8a542b5a570b85699cd63bd2bc646dbcb9f311d83bc4 - languageName: node - linkType: hard - -"environment@npm:^1.0.0": - version: 1.1.0 - resolution: "environment@npm:1.1.0" - checksum: 10c0/fb26434b0b581ab397039e51ff3c92b34924a98b2039dcb47e41b7bca577b9dbf134a8eadb364415c74464b682e2d3afe1a4c0eb9873dc44ea814c5d3103331d - languageName: node - linkType: hard - -"error-ex@npm:^1.3.1": - version: 1.3.4 - resolution: "error-ex@npm:1.3.4" - dependencies: - is-arrayish: "npm:^0.2.1" - checksum: 10c0/b9e34ff4778b8f3b31a8377e1c654456f4c41aeaa3d10a1138c3b7635d8b7b2e03eb2475d46d8ae055c1f180a1063e100bffabf64ea7e7388b37735df5328664 - languageName: node - linkType: hard - -"es-define-property@npm:^1.0.1": - version: 1.0.1 - resolution: "es-define-property@npm:1.0.1" - checksum: 10c0/3f54eb49c16c18707949ff25a1456728c883e81259f045003499efba399c08bad00deebf65cccde8c0e07908c1a225c9d472b7107e558f2a48e28d530e34527c - languageName: node - linkType: hard - -"es-errors@npm:^1.3.0": - version: 1.3.0 - resolution: "es-errors@npm:1.3.0" - checksum: 10c0/0a61325670072f98d8ae3b914edab3559b6caa980f08054a3b872052640d91da01d38df55df797fcc916389d77fc92b8d5906cf028f4db46d7e3003abecbca85 - languageName: node - linkType: hard - -"es-module-lexer@npm:^2.0.0": - version: 2.1.0 - resolution: "es-module-lexer@npm:2.1.0" - checksum: 10c0/93bcf2454fa72d67fe3ccd0abef8ce7933f5840a319513418a643dd8e9c6aa8f49709cecfae02ded722805dd327232d30723a807cc52e6809d6ac697c62c29fb - languageName: node - linkType: hard - -"es-object-atoms@npm:^1.0.0, es-object-atoms@npm:^1.1.1": - version: 1.1.2 - resolution: "es-object-atoms@npm:1.1.2" - dependencies: - es-errors: "npm:^1.3.0" - checksum: 10c0/1772861f094f739d6f41b579cfb9a18579daffeb434552a370a5fbef50a32d22227e27b63fdbb757b7ddd429d1b42fe52ccae7966d9302a2ec221b6f1b41bbc4 - languageName: node - linkType: hard - -"es-set-tostringtag@npm:^2.1.0": - version: 2.1.0 - resolution: "es-set-tostringtag@npm:2.1.0" - dependencies: - es-errors: "npm:^1.3.0" - get-intrinsic: "npm:^1.2.6" - has-tostringtag: "npm:^1.0.2" - hasown: "npm:^2.0.2" - checksum: 10c0/ef2ca9ce49afe3931cb32e35da4dcb6d86ab02592cfc2ce3e49ced199d9d0bb5085fc7e73e06312213765f5efa47cc1df553a6a5154584b21448e9fb8355b1af - languageName: node - linkType: hard - -"es-toolkit@npm:^1.39.10": - version: 1.47.0 - resolution: "es-toolkit@npm:1.47.0" - dependenciesMeta: - "@trivago/prettier-plugin-sort-imports@4.3.0": - unplugged: true - prettier-plugin-sort-re-exports@0.0.1: - unplugged: true - vitepress-plugin-sandpack@1.1.4: - unplugged: true - checksum: 10c0/6edc9709fccc409fa4adddd318971d8a18335de9225f2dc99021aacba44fe66a2437a831923589c643865caab261a087bd974338294a60bb5a74932caa102901 - languageName: node - linkType: hard - -"esbuild@npm:^0.28.1": - version: 0.28.1 - resolution: "esbuild@npm:0.28.1" - dependencies: - "@esbuild/aix-ppc64": "npm:0.28.1" - "@esbuild/android-arm": "npm:0.28.1" - "@esbuild/android-arm64": "npm:0.28.1" - "@esbuild/android-x64": "npm:0.28.1" - "@esbuild/darwin-arm64": "npm:0.28.1" - "@esbuild/darwin-x64": "npm:0.28.1" - "@esbuild/freebsd-arm64": "npm:0.28.1" - "@esbuild/freebsd-x64": "npm:0.28.1" - "@esbuild/linux-arm": "npm:0.28.1" - "@esbuild/linux-arm64": "npm:0.28.1" - "@esbuild/linux-ia32": "npm:0.28.1" - "@esbuild/linux-loong64": "npm:0.28.1" - "@esbuild/linux-mips64el": "npm:0.28.1" - "@esbuild/linux-ppc64": "npm:0.28.1" - "@esbuild/linux-riscv64": "npm:0.28.1" - "@esbuild/linux-s390x": "npm:0.28.1" - "@esbuild/linux-x64": "npm:0.28.1" - "@esbuild/netbsd-arm64": "npm:0.28.1" - "@esbuild/netbsd-x64": "npm:0.28.1" - "@esbuild/openbsd-arm64": "npm:0.28.1" - "@esbuild/openbsd-x64": "npm:0.28.1" - "@esbuild/openharmony-arm64": "npm:0.28.1" - "@esbuild/sunos-x64": "npm:0.28.1" - "@esbuild/win32-arm64": "npm:0.28.1" - "@esbuild/win32-ia32": "npm:0.28.1" - "@esbuild/win32-x64": "npm:0.28.1" - dependenciesMeta: - "@esbuild/aix-ppc64": - optional: true - "@esbuild/android-arm": - optional: true - "@esbuild/android-arm64": - optional: true - "@esbuild/android-x64": - optional: true - "@esbuild/darwin-arm64": - optional: true - "@esbuild/darwin-x64": - optional: true - "@esbuild/freebsd-arm64": - optional: true - "@esbuild/freebsd-x64": - optional: true - "@esbuild/linux-arm": - optional: true - "@esbuild/linux-arm64": - optional: true - "@esbuild/linux-ia32": - optional: true - "@esbuild/linux-loong64": - optional: true - "@esbuild/linux-mips64el": - optional: true - "@esbuild/linux-ppc64": - optional: true - "@esbuild/linux-riscv64": - optional: true - "@esbuild/linux-s390x": - optional: true - "@esbuild/linux-x64": - optional: true - "@esbuild/netbsd-arm64": - optional: true - "@esbuild/netbsd-x64": - optional: true - "@esbuild/openbsd-arm64": - optional: true - "@esbuild/openbsd-x64": - optional: true - "@esbuild/openharmony-arm64": - optional: true - "@esbuild/sunos-x64": - optional: true - "@esbuild/win32-arm64": - optional: true - "@esbuild/win32-ia32": - optional: true - "@esbuild/win32-x64": - optional: true - bin: - esbuild: bin/esbuild - checksum: 10c0/29cd456a79ce35ac2c7e05fe871330416b2c395c045d849653f843e51378d6e0d6e774d6dcd01b35f4e83238a29bf8decd04fcd34b3780c589a250b21e5f92bb - languageName: node - linkType: hard - -"escape-string-regexp@npm:^2.0.0": - version: 2.0.0 - resolution: "escape-string-regexp@npm:2.0.0" - checksum: 10c0/2530479fe8db57eace5e8646c9c2a9c80fa279614986d16dcc6bcaceb63ae77f05a851ba6c43756d816c61d7f4534baf56e3c705e3e0d884818a46808811c507 - languageName: node - linkType: hard - -"escape-string-regexp@npm:^4.0.0": - version: 4.0.0 - resolution: "escape-string-regexp@npm:4.0.0" - checksum: 10c0/9497d4dd307d845bd7f75180d8188bb17ea8c151c1edbf6b6717c100e104d629dc2dfb687686181b0f4b7d732c7dfdc4d5e7a8ff72de1b0ca283a75bbb3a9cd9 - languageName: node - linkType: hard - -"estree-walker@npm:^3.0.3": - version: 3.0.3 - resolution: "estree-walker@npm:3.0.3" - dependencies: - "@types/estree": "npm:^1.0.0" - checksum: 10c0/c12e3c2b2642d2bcae7d5aa495c60fa2f299160946535763969a1c83fc74518ffa9c2cd3a8b69ac56aea547df6a8aac25f729a342992ef0bbac5f1c73e78995d - languageName: node - linkType: hard - -"eventemitter3@npm:^3.1.0": - version: 3.1.2 - resolution: "eventemitter3@npm:3.1.2" - checksum: 10c0/c67262eccbf85848b7cc6d4abb6c6e34155e15686db2a01c57669fd0d44441a574a19d44d25948b442929e065774cbe5003d8e77eed47674fbf876ac77887793 - languageName: node - linkType: hard - -"expect-type@npm:^1.3.0": - version: 1.3.0 - resolution: "expect-type@npm:1.3.0" - checksum: 10c0/8412b3fe4f392c420ab41dae220b09700e4e47c639a29ba7ba2e83cc6cffd2b4926f7ac9e47d7e277e8f4f02acda76fd6931cb81fd2b382fa9477ef9ada953fd - languageName: node - linkType: hard - -"exponential-backoff@npm:^3.1.1": - version: 3.1.3 - resolution: "exponential-backoff@npm:3.1.3" - checksum: 10c0/77e3ae682b7b1f4972f563c6dbcd2b0d54ac679e62d5d32f3e5085feba20483cf28bd505543f520e287a56d4d55a28d7874299941faf637e779a1aa5994d1267 - languageName: node - linkType: hard - -"fast-string-truncated-width@npm:^3.0.2": - version: 3.0.3 - resolution: "fast-string-truncated-width@npm:3.0.3" - checksum: 10c0/043b8663397d14a3880ce4f3407bcda60b40db9bbeafe62863a35d1f9c69ea17c8da3fcd72de235553e6c9cd053128cde9e24ca0d4a7463208f48db3cd23d981 - languageName: node - linkType: hard - -"fast-string-width@npm:^3.0.2": - version: 3.0.2 - resolution: "fast-string-width@npm:3.0.2" - dependencies: - fast-string-truncated-width: "npm:^3.0.2" - checksum: 10c0/c8822d175315bb353ebe782b65214ac53b13e3bf704e03b132ea7bdfa8de6a636375b3ab7a4097545393d109381c37c4f387c72a462c90b61412dbc4632f39a7 - languageName: node - linkType: hard - -"fast-wrap-ansi@npm:^0.2.0": - version: 0.2.2 - resolution: "fast-wrap-ansi@npm:0.2.2" - dependencies: - fast-string-width: "npm:^3.0.2" - checksum: 10c0/1aa7be4f7cb86f4bdb14691cb6bcc0b8df8b3b89df142ade3ae1602332dcf6f990cd750a923cd581ca0847808cb4ec1aa5afaafa7a72f849e87a2a62c98fa370 - languageName: node - linkType: hard - -"fdir@npm:^6.5.0": - version: 6.5.0 - resolution: "fdir@npm:6.5.0" - peerDependencies: - picomatch: ^3 || ^4 - peerDependenciesMeta: - picomatch: - optional: true - checksum: 10c0/e345083c4306b3aed6cb8ec551e26c36bab5c511e99ea4576a16750ddc8d3240e63826cc624f5ae17ad4dc82e68a253213b60d556c11bfad064b7607847ed07f - languageName: node - linkType: hard - -"find-root@npm:^1.1.0": - version: 1.1.0 - resolution: "find-root@npm:1.1.0" - checksum: 10c0/1abc7f3bf2f8d78ff26d9e00ce9d0f7b32e5ff6d1da2857bcdf4746134c422282b091c672cde0572cac3840713487e0a7a636af9aa1b74cb11894b447a521efa - languageName: node - linkType: hard - -"follow-redirects@npm:^1.16.0": - version: 1.16.0 - resolution: "follow-redirects@npm:1.16.0" - peerDependenciesMeta: - debug: - optional: true - checksum: 10c0/a1e2900163e6f1b4d1ed5c221b607f41decbab65534c63fe7e287e40a5d552a6496e7d9d7d976fa4ba77b4c51c11e5e9f683f10b43011ea11e442ff128d0e181 - languageName: node - linkType: hard - -"form-data@npm:^4.0.5": - version: 4.0.6 - resolution: "form-data@npm:4.0.6" - dependencies: - asynckit: "npm:^0.4.0" - combined-stream: "npm:^1.0.8" - es-set-tostringtag: "npm:^2.1.0" - hasown: "npm:^2.0.4" - mime-types: "npm:^2.1.35" - checksum: 10c0/43947a77bf0ff45c6ceed789778982d47a3f3e720a74b71721174ebf3310a5f1a8be1d6b38a3ee3688e8a18a2c4273073ec0844cd37efda3eaf46d41c9c318ff - languageName: node - linkType: hard - -"fsevents@npm:~2.3.3": - version: 2.3.3 - resolution: "fsevents@npm:2.3.3" - dependencies: - node-gyp: "npm:latest" - checksum: 10c0/a1f0c44595123ed717febbc478aa952e47adfc28e2092be66b8ab1635147254ca6cfe1df792a8997f22716d4cbafc73309899ff7bfac2ac3ad8cf2e4ecc3ec60 - conditions: os=darwin - languageName: node - linkType: hard - -"fsevents@patch:fsevents@npm%3A~2.3.3#optional!builtin": - version: 2.3.3 - resolution: "fsevents@patch:fsevents@npm%3A2.3.3#optional!builtin::version=2.3.3&hash=df0bf1" - dependencies: - node-gyp: "npm:latest" - conditions: os=darwin - languageName: node - linkType: hard - -"function-bind@npm:^1.1.2": - version: 1.1.2 - resolution: "function-bind@npm:1.1.2" - checksum: 10c0/d8680ee1e5fcd4c197e4ac33b2b4dce03c71f4d91717292785703db200f5c21f977c568d28061226f9b5900cbcd2c84463646134fd5337e7925e0942bc3f46d5 - languageName: node - linkType: hard - -"generator-function@npm:^2.0.0": - version: 2.0.1 - resolution: "generator-function@npm:2.0.1" - checksum: 10c0/8a9f59df0f01cfefafdb3b451b80555e5cf6d76487095db91ac461a0e682e4ff7a9dbce15f4ecec191e53586d59eece01949e05a4b4492879600bbbe8e28d6b8 - languageName: node - linkType: hard - -"get-east-asian-width@npm:^1.0.0, get-east-asian-width@npm:^1.3.1, get-east-asian-width@npm:^1.5.0": - version: 1.6.0 - resolution: "get-east-asian-width@npm:1.6.0" - checksum: 10c0/7e72e9550fd49ca5b246f9af6bb2afc129c96412845ff6556b3274fd44817a381702ca17028efe9866b261a3d44254cbf21e6c90cf05b4b61675630af776d431 - languageName: node - linkType: hard - -"get-intrinsic@npm:^1.2.6": - version: 1.3.1 - resolution: "get-intrinsic@npm:1.3.1" - dependencies: - async-function: "npm:^1.0.0" - async-generator-function: "npm:^1.0.0" - call-bind-apply-helpers: "npm:^1.0.2" - es-define-property: "npm:^1.0.1" - es-errors: "npm:^1.3.0" - es-object-atoms: "npm:^1.1.1" - function-bind: "npm:^1.1.2" - generator-function: "npm:^2.0.0" - get-proto: "npm:^1.0.1" - gopd: "npm:^1.2.0" - has-symbols: "npm:^1.1.0" - hasown: "npm:^2.0.2" - math-intrinsics: "npm:^1.1.0" - checksum: 10c0/9f4ab0cf7efe0fd2c8185f52e6f637e708f3a112610c88869f8f041bb9ecc2ce44bf285dfdbdc6f4f7c277a5b88d8e94a432374d97cca22f3de7fc63795deb5d - languageName: node - linkType: hard - -"get-proto@npm:^1.0.1": - version: 1.0.1 - resolution: "get-proto@npm:1.0.1" - dependencies: - dunder-proto: "npm:^1.0.1" - es-object-atoms: "npm:^1.0.0" - checksum: 10c0/9224acb44603c5526955e83510b9da41baf6ae73f7398875fba50edc5e944223a89c4a72b070fcd78beb5f7bdda58ecb6294adc28f7acfc0da05f76a2399643c - languageName: node - linkType: hard - -"globrex@npm:^0.1.2": - version: 0.1.2 - resolution: "globrex@npm:0.1.2" - checksum: 10c0/a54c029520cf58bda1d8884f72bd49b4cd74e977883268d931fd83bcbd1a9eb96d57c7dbd4ad80148fb9247467ebfb9b215630b2ed7563b2a8de02e1ff7f89d1 - languageName: node - linkType: hard - -"gopd@npm:^1.2.0": - version: 1.2.0 - resolution: "gopd@npm:1.2.0" - checksum: 10c0/50fff1e04ba2b7737c097358534eacadad1e68d24cccee3272e04e007bed008e68d2614f3987788428fd192a5ae3889d08fb2331417e4fc4a9ab366b2043cead - languageName: node - linkType: hard - -"graceful-fs@npm:^4.2.6": - version: 4.2.11 - resolution: "graceful-fs@npm:4.2.11" - checksum: 10c0/386d011a553e02bc594ac2ca0bd6d9e4c22d7fa8cfbfc448a6d148c59ea881b092db9dbe3547ae4b88e55f1b01f7c4a2ecc53b310c042793e63aa44cf6c257f2 - languageName: node - linkType: hard - -"graphql-query-batcher@npm:^1.0.1": - version: 1.0.1 - resolution: "graphql-query-batcher@npm:1.0.1" - checksum: 10c0/804d0f4064721a2116a16b9eac422e9233e85f4ab5b250cb8f83662725658ffde35779a8ae8211037f3dd2f9717de8cb63b394ad8057ce22171a83db2471196a - languageName: node - linkType: hard - -"graphql-sse@npm:^2.5.4": - version: 2.6.0 - resolution: "graphql-sse@npm:2.6.0" - peerDependencies: - graphql: ">=0.11 <=16" - checksum: 10c0/e05f0b5c8539d61e5ce34af8e0bb418c02bf922d6a7f9232a9abd53c77df5654684ca14f674ac771645c8fba9c37ce666c5d06f46eef54ea07e63653468065b0 - languageName: node - linkType: hard - -"graphql@npm:^16.8.1": - version: 16.14.2 - resolution: "graphql@npm:16.14.2" - checksum: 10c0/a95a96961eaff55cc9fe9d31fae6f33499ac988b972d07ea5085024cb1333f515b902f376e7393a5489aa82200a8aff3eb96580e4d1b69d702ed19b6eb1ce97a - languageName: node - linkType: hard - -"has-symbols@npm:^1.0.3, has-symbols@npm:^1.1.0": - version: 1.1.0 - resolution: "has-symbols@npm:1.1.0" - checksum: 10c0/dde0a734b17ae51e84b10986e651c664379018d10b91b6b0e9b293eddb32f0f069688c841fb40f19e9611546130153e0a2a48fd7f512891fb000ddfa36f5a20e - languageName: node - linkType: hard - -"has-tostringtag@npm:^1.0.2": - version: 1.0.2 - resolution: "has-tostringtag@npm:1.0.2" - dependencies: - has-symbols: "npm:^1.0.3" - checksum: 10c0/a8b166462192bafe3d9b6e420a1d581d93dd867adb61be223a17a8d6dad147aa77a8be32c961bb2f27b3ef893cae8d36f564ab651f5e9b7938ae86f74027c48c - languageName: node - linkType: hard - -"hasown@npm:^2.0.2, hasown@npm:^2.0.3, hasown@npm:^2.0.4": - version: 2.0.4 - resolution: "hasown@npm:2.0.4" - dependencies: - function-bind: "npm:^1.1.2" - checksum: 10c0/2d8de939e270b70618f8cebb69746620db10617dbb495bc66ddad326955ea24d3ca4af133aff3eb7c1853e0218f867bc2b050ec26fe02e3aea58f880ffc5e506 - languageName: node - linkType: hard - -"hoist-non-react-statics@npm:^3.3.1": - version: 3.3.2 - resolution: "hoist-non-react-statics@npm:3.3.2" - dependencies: - react-is: "npm:^16.7.0" - checksum: 10c0/fe0889169e845d738b59b64badf5e55fa3cf20454f9203d1eb088df322d49d4318df774828e789898dcb280e8a5521bb59b3203385662ca5e9218a6ca5820e74 - languageName: node - linkType: hard - -"https-proxy-agent@npm:^5.0.1": - version: 5.0.1 - resolution: "https-proxy-agent@npm:5.0.1" - dependencies: - agent-base: "npm:6" - debug: "npm:4" - checksum: 10c0/6dd639f03434003577c62b27cafdb864784ef19b2de430d8ae2a1d45e31c4fd60719e5637b44db1a88a046934307da7089e03d6089ec3ddacc1189d8de8897d1 - languageName: node - linkType: hard - -"iconv-lite@npm:^0.7.2": - version: 0.7.2 - resolution: "iconv-lite@npm:0.7.2" - dependencies: - safer-buffer: "npm:>= 2.1.2 < 3.0.0" - checksum: 10c0/3c228920f3bd307f56bf8363706a776f4a060eb042f131cd23855ceca962951b264d0997ab38a1ad340e1c5df8499ed26e1f4f0db6b2a2ad9befaff22f14b722 - languageName: node - linkType: hard - -"import-fresh@npm:^3.2.1": - version: 3.3.1 - resolution: "import-fresh@npm:3.3.1" - dependencies: - parent-module: "npm:^1.0.0" - resolve-from: "npm:^4.0.0" - checksum: 10c0/bf8cc494872fef783249709385ae883b447e3eb09db0ebd15dcead7d9afe7224dad7bd7591c6b73b0b19b3c0f9640eb8ee884f01cfaf2887ab995b0b36a0cbec - languageName: node - linkType: hard - -"indent-string@npm:^5.0.0": - version: 5.0.0 - resolution: "indent-string@npm:5.0.0" - checksum: 10c0/8ee77b57d92e71745e133f6f444d6fa3ed503ad0e1bcd7e80c8da08b42375c07117128d670589725ed07b1978065803fa86318c309ba45415b7fe13e7f170220 - languageName: node - linkType: hard - -"ink@npm:^6.8.0": - version: 6.8.0 - resolution: "ink@npm:6.8.0" - dependencies: - "@alcalzone/ansi-tokenize": "npm:^0.2.4" - ansi-escapes: "npm:^7.3.0" - ansi-styles: "npm:^6.2.1" - auto-bind: "npm:^5.0.1" - chalk: "npm:^5.6.0" - cli-boxes: "npm:^3.0.0" - cli-cursor: "npm:^4.0.0" - cli-truncate: "npm:^5.1.1" - code-excerpt: "npm:^4.0.0" - es-toolkit: "npm:^1.39.10" - indent-string: "npm:^5.0.0" - is-in-ci: "npm:^2.0.0" - patch-console: "npm:^2.0.0" - react-reconciler: "npm:^0.33.0" - scheduler: "npm:^0.27.0" - signal-exit: "npm:^3.0.7" - slice-ansi: "npm:^8.0.0" - stack-utils: "npm:^2.0.6" - string-width: "npm:^8.1.1" - terminal-size: "npm:^4.0.1" - type-fest: "npm:^5.4.1" - widest-line: "npm:^6.0.0" - wrap-ansi: "npm:^9.0.0" - ws: "npm:^8.18.0" - yoga-layout: "npm:~3.2.1" - peerDependencies: - "@types/react": ">=19.0.0" - react: ">=19.0.0" - react-devtools-core: ">=6.1.2" - peerDependenciesMeta: - "@types/react": - optional: true - react-devtools-core: - optional: true - checksum: 10c0/50500e547fdf6a1f1d836d6befbd4770e3ab649ef0be1884500a6da411fb68a90e22dd7dcc9c404911d30e9f87506b3b9d8e997c6c6ceac85ee054b4dadefaff - languageName: node - linkType: hard - -"inquirer@npm:^14.0.0": - version: 14.0.2 - resolution: "inquirer@npm:14.0.2" - dependencies: - "@inquirer/ansi": "npm:^2.0.7" - "@inquirer/core": "npm:^11.2.1" - "@inquirer/prompts": "npm:^8.5.2" - "@inquirer/type": "npm:^4.0.7" - mute-stream: "npm:^3.0.0" - run-async: "npm:^4.0.6" - peerDependencies: - "@types/node": ">=18" - peerDependenciesMeta: - "@types/node": - optional: true - checksum: 10c0/31ec80b7d599dcb19568540d694865b1da116f701b75503a23ed38189d96fca70eb1fb6ccb1dbd994f6f79d407d51dc1a94d06dcef5370644ce736792414781b - languageName: node - linkType: hard - -"is-arrayish@npm:^0.2.1": - version: 0.2.1 - resolution: "is-arrayish@npm:0.2.1" - checksum: 10c0/e7fb686a739068bb70f860b39b67afc62acc62e36bb61c5f965768abce1873b379c563e61dd2adad96ebb7edf6651111b385e490cf508378959b0ed4cac4e729 - languageName: node - linkType: hard - -"is-core-module@npm:^2.16.1": - version: 2.16.2 - resolution: "is-core-module@npm:2.16.2" - dependencies: - hasown: "npm:^2.0.3" - checksum: 10c0/14b4258390283709c15476d023ec173e27458d5d014ccdb8ed39d576e551c3fa45498b7c9fe178f1529c4cb2648ddd58852a6a62107a019f6e349529f277518a - languageName: node - linkType: hard - -"is-fullwidth-code-point@npm:^5.0.0, is-fullwidth-code-point@npm:^5.1.0": - version: 5.1.0 - resolution: "is-fullwidth-code-point@npm:5.1.0" - dependencies: - get-east-asian-width: "npm:^1.3.1" - checksum: 10c0/c1172c2e417fb73470c56c431851681591f6a17233603a9e6f94b7ba870b2e8a5266506490573b607fb1081318589372034aa436aec07b465c2029c0bc7f07a4 - languageName: node - linkType: hard - -"is-in-ci@npm:^2.0.0": - version: 2.0.0 - resolution: "is-in-ci@npm:2.0.0" - bin: - is-in-ci: cli.js - checksum: 10c0/1e1d1056939a681e8206035de5ad84e0404556eaa7622bb55f0f1868b9788bff3df427bc0b1ed5a172623154a90fcb1e759a230817cd73d09435543ae3c71feb - languageName: node - linkType: hard - -"isexe@npm:^4.0.0": - version: 4.0.0 - resolution: "isexe@npm:4.0.0" - checksum: 10c0/5884815115bceac452877659a9c7726382531592f43dc29e5d48b7c4100661aed54018cb90bd36cb2eaeba521092570769167acbb95c18d39afdccbcca06c5ce - languageName: node - linkType: hard - -"isomorphic-unfetch@npm:^3.0.0": - version: 3.1.0 - resolution: "isomorphic-unfetch@npm:3.1.0" - dependencies: - node-fetch: "npm:^2.6.1" - unfetch: "npm:^4.2.0" - checksum: 10c0/d3b61fca06304db692b7f76bdfd3a00f410e42cfa7403c3b250546bf71589d18cf2f355922f57198e4cc4a9872d3647b20397a5c3edf1a347c90d57c83cf2a89 - languageName: node - linkType: hard - -"iterall@npm:^1.2.1": - version: 1.3.0 - resolution: "iterall@npm:1.3.0" - checksum: 10c0/40de624e5fe937c4c0e511981b91caea9ff2142bfc0316cccc8506eaa03aa253820cc17c5bc5f0a98706c7268a373e5ebee9af9a0c8a359730cf7c05938b57b5 - languageName: node - linkType: hard - -"js-tokens@npm:^4.0.0": - version: 4.0.0 - resolution: "js-tokens@npm:4.0.0" - checksum: 10c0/e248708d377aa058eacf2037b07ded847790e6de892bbad3dac0abba2e759cb9f121b00099a65195616badcb6eca8d14d975cb3e89eb1cfda644756402c8aeed - languageName: node - linkType: hard - -"jsesc@npm:^3.0.2": - version: 3.1.0 - resolution: "jsesc@npm:3.1.0" - bin: - jsesc: bin/jsesc - checksum: 10c0/531779df5ec94f47e462da26b4cbf05eb88a83d9f08aac2ba04206508fc598527a153d08bd462bae82fc78b3eaa1a908e1a4a79f886e9238641c4cdefaf118b1 - languageName: node - linkType: hard - -"json-parse-even-better-errors@npm:^2.3.0": - version: 2.3.1 - resolution: "json-parse-even-better-errors@npm:2.3.1" - checksum: 10c0/140932564c8f0b88455432e0f33c4cb4086b8868e37524e07e723f4eaedb9425bdc2bafd71bd1d9765bd15fd1e2d126972bc83990f55c467168c228c24d665f3 - languageName: node - linkType: hard - -"jsonc-parser@npm:^3.2.0": - version: 3.3.1 - resolution: "jsonc-parser@npm:3.3.1" - checksum: 10c0/269c3ae0a0e4f907a914bf334306c384aabb9929bd8c99f909275ebd5c2d3bc70b9bcd119ad794f339dec9f24b6a4ee9cd5a8ab2e6435e730ad4075388fc2ab6 - languageName: node - linkType: hard - -"lightningcss-android-arm64@npm:1.32.0": - version: 1.32.0 - resolution: "lightningcss-android-arm64@npm:1.32.0" - conditions: os=android & cpu=arm64 - languageName: node - linkType: hard - -"lightningcss-darwin-arm64@npm:1.32.0": - version: 1.32.0 - resolution: "lightningcss-darwin-arm64@npm:1.32.0" - conditions: os=darwin & cpu=arm64 - languageName: node - linkType: hard - -"lightningcss-darwin-x64@npm:1.32.0": - version: 1.32.0 - resolution: "lightningcss-darwin-x64@npm:1.32.0" - conditions: os=darwin & cpu=x64 - languageName: node - linkType: hard - -"lightningcss-freebsd-x64@npm:1.32.0": - version: 1.32.0 - resolution: "lightningcss-freebsd-x64@npm:1.32.0" - conditions: os=freebsd & cpu=x64 - languageName: node - linkType: hard - -"lightningcss-linux-arm-gnueabihf@npm:1.32.0": - version: 1.32.0 - resolution: "lightningcss-linux-arm-gnueabihf@npm:1.32.0" - conditions: os=linux & cpu=arm - languageName: node - linkType: hard - -"lightningcss-linux-arm64-gnu@npm:1.32.0": - version: 1.32.0 - resolution: "lightningcss-linux-arm64-gnu@npm:1.32.0" - conditions: os=linux & cpu=arm64 & libc=glibc - languageName: node - linkType: hard - -"lightningcss-linux-arm64-musl@npm:1.32.0": - version: 1.32.0 - resolution: "lightningcss-linux-arm64-musl@npm:1.32.0" - conditions: os=linux & cpu=arm64 & libc=musl - languageName: node - linkType: hard - -"lightningcss-linux-x64-gnu@npm:1.32.0": - version: 1.32.0 - resolution: "lightningcss-linux-x64-gnu@npm:1.32.0" - conditions: os=linux & cpu=x64 & libc=glibc - languageName: node - linkType: hard - -"lightningcss-linux-x64-musl@npm:1.32.0": - version: 1.32.0 - resolution: "lightningcss-linux-x64-musl@npm:1.32.0" - conditions: os=linux & cpu=x64 & libc=musl - languageName: node - linkType: hard - -"lightningcss-win32-arm64-msvc@npm:1.32.0": - version: 1.32.0 - resolution: "lightningcss-win32-arm64-msvc@npm:1.32.0" - conditions: os=win32 & cpu=arm64 - languageName: node - linkType: hard - -"lightningcss-win32-x64-msvc@npm:1.32.0": - version: 1.32.0 - resolution: "lightningcss-win32-x64-msvc@npm:1.32.0" - conditions: os=win32 & cpu=x64 - languageName: node - linkType: hard - -"lightningcss@npm:^1.32.0": - version: 1.32.0 - resolution: "lightningcss@npm:1.32.0" - dependencies: - detect-libc: "npm:^2.0.3" - lightningcss-android-arm64: "npm:1.32.0" - lightningcss-darwin-arm64: "npm:1.32.0" - lightningcss-darwin-x64: "npm:1.32.0" - lightningcss-freebsd-x64: "npm:1.32.0" - lightningcss-linux-arm-gnueabihf: "npm:1.32.0" - lightningcss-linux-arm64-gnu: "npm:1.32.0" - lightningcss-linux-arm64-musl: "npm:1.32.0" - lightningcss-linux-x64-gnu: "npm:1.32.0" - lightningcss-linux-x64-musl: "npm:1.32.0" - lightningcss-win32-arm64-msvc: "npm:1.32.0" - lightningcss-win32-x64-msvc: "npm:1.32.0" - dependenciesMeta: - lightningcss-android-arm64: - optional: true - lightningcss-darwin-arm64: - optional: true - lightningcss-darwin-x64: - optional: true - lightningcss-freebsd-x64: - optional: true - lightningcss-linux-arm-gnueabihf: - optional: true - lightningcss-linux-arm64-gnu: - optional: true - lightningcss-linux-arm64-musl: - optional: true - lightningcss-linux-x64-gnu: - optional: true - lightningcss-linux-x64-musl: - optional: true - lightningcss-win32-arm64-msvc: - optional: true - lightningcss-win32-x64-msvc: - optional: true - checksum: 10c0/70945bd55097af46fc9fab7f5ed09cd5869d85940a2acab7ee06d0117004a1d68155708a2d462531cea2fc3c67aefc9333a7068c80b0b78dd404c16838809e03 - languageName: node - linkType: hard - -"lines-and-columns@npm:^1.1.6": - version: 1.2.4 - resolution: "lines-and-columns@npm:1.2.4" - checksum: 10c0/3da6ee62d4cd9f03f5dc90b4df2540fb85b352081bee77fe4bbcd12c9000ead7f35e0a38b8d09a9bb99b13223446dd8689ff3c4959807620726d788701a83d2d - languageName: node - linkType: hard - -"lodash@npm:^4.17.20, lodash@npm:^4.17.21": - version: 4.18.1 - resolution: "lodash@npm:4.18.1" - checksum: 10c0/757228fc68805c59789e82185135cf85f05d0b2d3d54631d680ca79ec21944ec8314d4533639a14b8bcfbd97a517e78960933041a5af17ecb693ec6eecb99a27 - languageName: node - linkType: hard - -"magic-string@npm:^0.30.21": - version: 0.30.21 - resolution: "magic-string@npm:0.30.21" - dependencies: - "@jridgewell/sourcemap-codec": "npm:^1.5.5" - checksum: 10c0/299378e38f9a270069fc62358522ddfb44e94244baa0d6a8980ab2a9b2490a1d03b236b447eee309e17eb3bddfa482c61259d47960eb018a904f0ded52780c4a - languageName: node - linkType: hard - -"math-intrinsics@npm:^1.1.0": - version: 1.1.0 - resolution: "math-intrinsics@npm:1.1.0" - checksum: 10c0/7579ff94e899e2f76ab64491d76cf606274c874d8f2af4a442c016bd85688927fcfca157ba6bf74b08e9439dc010b248ce05b96cc7c126a354c3bae7fcb48b7f - languageName: node - linkType: hard - -"mime-db@npm:1.52.0": - version: 1.52.0 - resolution: "mime-db@npm:1.52.0" - checksum: 10c0/0557a01deebf45ac5f5777fe7740b2a5c309c6d62d40ceab4e23da9f821899ce7a900b7ac8157d4548ddbb7beffe9abc621250e6d182b0397ec7f10c7b91a5aa - languageName: node - linkType: hard - -"mime-types@npm:^2.1.35": - version: 2.1.35 - resolution: "mime-types@npm:2.1.35" - dependencies: - mime-db: "npm:1.52.0" - checksum: 10c0/82fb07ec56d8ff1fc999a84f2f217aa46cb6ed1033fefaabd5785b9a974ed225c90dc72fff460259e66b95b73648596dbcc50d51ed69cdf464af2d237d3149b2 - languageName: node - linkType: hard - -"mimic-fn@npm:^2.1.0": - version: 2.1.0 - resolution: "mimic-fn@npm:2.1.0" - checksum: 10c0/b26f5479d7ec6cc2bce275a08f146cf78f5e7b661b18114e2506dd91ec7ec47e7a25bf4360e5438094db0560bcc868079fb3b1fb3892b833c1ecbf63f80c95a4 - languageName: node - linkType: hard - -"minipass@npm:^7.0.4, minipass@npm:^7.1.2": - version: 7.1.3 - resolution: "minipass@npm:7.1.3" - checksum: 10c0/539da88daca16533211ea5a9ee98dc62ff5742f531f54640dd34429e621955e91cc280a91a776026264b7f9f6735947629f920944e9c1558369e8bf22eb33fbb - languageName: node - linkType: hard - -"minizlib@npm:^3.1.0": - version: 3.1.0 - resolution: "minizlib@npm:3.1.0" - dependencies: - minipass: "npm:^7.1.2" - checksum: 10c0/5aad75ab0090b8266069c9aabe582c021ae53eb33c6c691054a13a45db3b4f91a7fb1bd79151e6b4e9e9a86727b522527c0a06ec7d45206b745d54cd3097bcec - languageName: node - linkType: hard - -"ms@npm:^2.1.3": - version: 2.1.3 - resolution: "ms@npm:2.1.3" - checksum: 10c0/d924b57e7312b3b63ad21fc5b3dc0af5e78d61a1fc7cfb5457edaf26326bf62be5307cc87ffb6862ef1c2b33b0233cdb5d4f01c4c958cc0d660948b65a287a48 - languageName: node - linkType: hard - -"mute-stream@npm:^3.0.0": - version: 3.0.0 - resolution: "mute-stream@npm:3.0.0" - checksum: 10c0/12cdb36a101694c7a6b296632e6d93a30b74401873cf7507c88861441a090c71c77a58f213acadad03bc0c8fa186639dec99d68a14497773a8744320c136e701 - languageName: node - linkType: hard - -"nanoid@npm:^3.3.12": - version: 3.3.12 - resolution: "nanoid@npm:3.3.12" - bin: - nanoid: bin/nanoid.cjs - checksum: 10c0/ba142b7b39e11e80c16dd74b0365d407880c87c1cf7e1480956981ae940ee36060fa5b6f092cd1e315184dd19244c657bd017d03327bd3c62247d691c5e8edfb - languageName: node - linkType: hard - -"node-fetch@npm:^2.6.1": - version: 2.7.0 - resolution: "node-fetch@npm:2.7.0" - dependencies: - whatwg-url: "npm:^5.0.0" - peerDependencies: - encoding: ^0.1.0 - peerDependenciesMeta: - encoding: - optional: true - checksum: 10c0/b55786b6028208e6fbe594ccccc213cab67a72899c9234eb59dba51062a299ea853210fcf526998eaa2867b0963ad72338824450905679ff0fa304b8c5093ae8 - languageName: node - linkType: hard - -"node-gyp@npm:latest": - version: 12.4.0 - resolution: "node-gyp@npm:12.4.0" - dependencies: - env-paths: "npm:^2.2.0" - exponential-backoff: "npm:^3.1.1" - graceful-fs: "npm:^4.2.6" - nopt: "npm:^9.0.0" - proc-log: "npm:^6.0.0" - semver: "npm:^7.3.5" - tar: "npm:^7.5.4" - tinyglobby: "npm:^0.2.12" - undici: "npm:^6.25.0" - which: "npm:^6.0.0" - bin: - node-gyp: bin/node-gyp.js - checksum: 10c0/9acb7c798e124275a6f9c1f7eb64b5abd6196bb885a3945fb44ee0dccf435514e88cdfb0f228ee7ff76ef25107c1f39ff37a067bf92fd00b9aff9234db29ff9e - languageName: node - linkType: hard - -"nopt@npm:^9.0.0": - version: 9.0.0 - resolution: "nopt@npm:9.0.0" - dependencies: - abbrev: "npm:^4.0.0" - bin: - nopt: bin/nopt.js - checksum: 10c0/1822eb6f9b020ef6f7a7516d7b64a8036e09666ea55ac40416c36e4b2b343122c3cff0e2f085675f53de1d2db99a2a89a60ccea1d120bcd6a5347bf6ceb4a7fd - languageName: node - linkType: hard - -"obug@npm:^2.1.1": - version: 2.1.3 - resolution: "obug@npm:2.1.3" - checksum: 10c0/cb8187fed0a5fc8445507c950e89f3c1bd43895658c398b5803f6b7804dfa0c562975ecce1e67f3d9247d521452a5bfade9e0e951cc0326b7444272f7c24d25f - languageName: node - linkType: hard - -"onetime@npm:^5.1.0": - version: 5.1.2 - resolution: "onetime@npm:5.1.2" - dependencies: - mimic-fn: "npm:^2.1.0" - checksum: 10c0/ffcef6fbb2692c3c40749f31ea2e22677a876daea92959b8a80b521d95cca7a668c884d8b2045d1d8ee7d56796aa405c405462af112a1477594cc63531baeb8f - languageName: node - linkType: hard - -"oxlint@npm:^0.16.0": - version: 0.16.12 - resolution: "oxlint@npm:0.16.12" - dependencies: - "@oxlint/darwin-arm64": "npm:0.16.12" - "@oxlint/darwin-x64": "npm:0.16.12" - "@oxlint/linux-arm64-gnu": "npm:0.16.12" - "@oxlint/linux-arm64-musl": "npm:0.16.12" - "@oxlint/linux-x64-gnu": "npm:0.16.12" - "@oxlint/linux-x64-musl": "npm:0.16.12" - "@oxlint/win32-arm64": "npm:0.16.12" - "@oxlint/win32-x64": "npm:0.16.12" - dependenciesMeta: - "@oxlint/darwin-arm64": - optional: true - "@oxlint/darwin-x64": - optional: true - "@oxlint/linux-arm64-gnu": - optional: true - "@oxlint/linux-arm64-musl": - optional: true - "@oxlint/linux-x64-gnu": - optional: true - "@oxlint/linux-x64-musl": - optional: true - "@oxlint/win32-arm64": - optional: true - "@oxlint/win32-x64": - optional: true - bin: - oxc_language_server: bin/oxc_language_server - oxlint: bin/oxlint - checksum: 10c0/209c3484039c4f1fdd340689e81be93a70fdf74e8c111731e70d81580d223b0029439107c95fa4118cec3c9b03ba6c56624d3dcb8cdc79af222da3561122b3e7 - languageName: node - linkType: hard - -"parent-module@npm:^1.0.0": - version: 1.0.1 - resolution: "parent-module@npm:1.0.1" - dependencies: - callsites: "npm:^3.0.0" - checksum: 10c0/c63d6e80000d4babd11978e0d3fee386ca7752a02b035fd2435960ffaa7219dc42146f07069fb65e6e8bf1caef89daf9af7535a39bddf354d78bf50d8294f556 - languageName: node - linkType: hard - -"parse-json@npm:^5.0.0": - version: 5.2.0 - resolution: "parse-json@npm:5.2.0" - dependencies: - "@babel/code-frame": "npm:^7.0.0" - error-ex: "npm:^1.3.1" - json-parse-even-better-errors: "npm:^2.3.0" - lines-and-columns: "npm:^1.1.6" - checksum: 10c0/77947f2253005be7a12d858aedbafa09c9ae39eb4863adf330f7b416ca4f4a08132e453e08de2db46459256fb66afaac5ee758b44fe6541b7cdaf9d252e59585 - languageName: node - linkType: hard - -"patch-console@npm:^2.0.0": - version: 2.0.0 - resolution: "patch-console@npm:2.0.0" - checksum: 10c0/486602591a0af7af8d4c76d8eea42cad32b6de7200488819c6383c75e43733ca7bdc80e30f2e68ce05f06a1607cce1683a1706c6672ca27dada1921b366e8f1c - languageName: node - linkType: hard - -"path-parse@npm:^1.0.7": - version: 1.0.7 - resolution: "path-parse@npm:1.0.7" - checksum: 10c0/11ce261f9d294cc7a58d6a574b7f1b935842355ec66fba3c3fd79e0f036462eaf07d0aa95bb74ff432f9afef97ce1926c720988c6a7451d8a584930ae7de86e1 - languageName: node - linkType: hard - -"path-type@npm:^4.0.0": - version: 4.0.0 - resolution: "path-type@npm:4.0.0" - checksum: 10c0/666f6973f332f27581371efaf303fd6c272cc43c2057b37aa99e3643158c7e4b2626549555d88626e99ea9e046f82f32e41bbde5f1508547e9a11b149b52387c - languageName: node - linkType: hard - -"pathe@npm:^2.0.3": - version: 2.0.3 - resolution: "pathe@npm:2.0.3" - checksum: 10c0/c118dc5a8b5c4166011b2b70608762e260085180bb9e33e80a50dcdb1e78c010b1624f4280c492c92b05fc276715a4c357d1f9edc570f8f1b3d90b6839ebaca1 - languageName: node - linkType: hard - -"picocolors@npm:^1.1.1": - version: 1.1.1 - resolution: "picocolors@npm:1.1.1" - checksum: 10c0/e2e3e8170ab9d7c7421969adaa7e1b31434f789afb9b3f115f6b96d91945041ac3ceb02e9ec6fe6510ff036bcc0bf91e69a1772edc0b707e12b19c0f2d6bcf58 - languageName: node - linkType: hard - -"picomatch@npm:^4.0.3, picomatch@npm:^4.0.4": - version: 4.0.4 - resolution: "picomatch@npm:4.0.4" - checksum: 10c0/e2c6023372cc7b5764719a5ffb9da0f8e781212fa7ca4bd0562db929df8e117460f00dff3cb7509dacfc06b86de924b247f504d0ce1806a37fac4633081466b0 - languageName: node - linkType: hard - -"postcss@npm:^8.5.15": - version: 8.5.15 - resolution: "postcss@npm:8.5.15" - dependencies: - nanoid: "npm:^3.3.12" - picocolors: "npm:^1.1.1" - source-map-js: "npm:^1.2.1" - checksum: 10c0/7f2e63ae22fbe43aace1bf652bd99da4e90737c64194d49e51ddc9cd0f9e51ff2861a7d734379b494deffa03a880a5c65eec70bc29ee9ebaa7136dde3eee8f31 - languageName: node - linkType: hard - -"preact@npm:^10.28.3": - version: 10.29.2 - resolution: "preact@npm:10.29.2" - checksum: 10c0/2ee161274e475804524608d42bba8c79920636b322293248796a9e5bf4c3332336f8c0eca1b1566ed09cdee95e408eb20b5b7613d95820ea6b6eb8fc090ef60a - languageName: node - linkType: hard - -"prettier@npm:^3.8.3": - version: 3.8.4 - resolution: "prettier@npm:3.8.4" - bin: - prettier: bin/prettier.cjs - checksum: 10c0/b90a0cbe75b88ac0af9c13fe0f359bd19926fabccd88483227b21f71f0c1cc42da056fc1ac3a361e665577c568371d5ccfb2c62c31c8a1186f8d1bd531a063e9 - languageName: node - linkType: hard - -"proc-log@npm:^6.0.0": - version: 6.1.0 - resolution: "proc-log@npm:6.1.0" - checksum: 10c0/4f178d4062733ead9d71a9b1ab24ebcecdfe2250916a5b1555f04fe2eda972a0ec76fbaa8df1ad9c02707add6749219d118a4fc46dc56bdfe4dde4b47d80bb82 - languageName: node - linkType: hard - -"proxy-from-env@npm:^2.1.0": - version: 2.1.0 - resolution: "proxy-from-env@npm:2.1.0" - checksum: 10c0/ed01729fd4d094eab619cd7e17ce3698b3413b31eb102c4904f9875e677cd207392795d5b4adee9cec359dfd31c44d5ad7595a3a3ad51c40250e141512281c58 - languageName: node - linkType: hard - -"react-dom@npm:^19.0.0, react-dom@npm:^19.2.0": - version: 19.2.7 - resolution: "react-dom@npm:19.2.7" - dependencies: - scheduler: "npm:^0.27.0" - peerDependencies: - react: ^19.2.7 - checksum: 10c0/970ff600f6e80d47d39e2f226f12f226173b3cba3382efc97c5f0cd663de9af38c7a4c11c213fb936094faeac83060d660247accaa96b752180d5b951b9cfecb - languageName: node - linkType: hard - -"react-is@npm:^16.7.0": - version: 16.13.1 - resolution: "react-is@npm:16.13.1" - checksum: 10c0/33977da7a5f1a287936a0c85639fec6ca74f4f15ef1e59a6bc20338fc73dc69555381e211f7a3529b8150a1f71e4225525b41b60b52965bda53ce7d47377ada1 - languageName: node - linkType: hard - -"react-reconciler@npm:^0.33.0": - version: 0.33.0 - resolution: "react-reconciler@npm:0.33.0" - dependencies: - scheduler: "npm:^0.27.0" - peerDependencies: - react: ^19.2.0 - checksum: 10c0/3f7b27ea8d0ff4c8bf0e402a285e1af9b7d0e6f4c1a70a28f4384938bc1130bc82a90a31df0b79ef5e380e2e55e2598bd90b4dbf802b1203d735ba0355817d3a - languageName: node - linkType: hard - -"react@npm:^19.0.0, react@npm:^19.2.0": - version: 19.2.7 - resolution: "react@npm:19.2.7" - checksum: 10c0/0bd0e2f1bbd4ba97561c6597bf8a5fec05e6476fe61e165c1065598d16668efc6715205599c94d3ddd49d36cb0f21cbf1b9bcc18ee840b805ce222c3e8d558ac - languageName: node - linkType: hard - -"readdirp@npm:^4.0.1": - version: 4.1.2 - resolution: "readdirp@npm:4.1.2" - checksum: 10c0/60a14f7619dec48c9c850255cd523e2717001b0e179dc7037cfa0895da7b9e9ab07532d324bfb118d73a710887d1e35f79c495fa91582784493e085d18c72c62 - languageName: node - linkType: hard - -"resolve-from@npm:^4.0.0": - version: 4.0.0 - resolution: "resolve-from@npm:4.0.0" - checksum: 10c0/8408eec31a3112ef96e3746c37be7d64020cda07c03a920f5024e77290a218ea758b26ca9529fd7b1ad283947f34b2291c1c0f6aa0ed34acfdda9c6014c8d190 - languageName: node - linkType: hard - -"resolve@npm:^1.19.0": - version: 1.22.12 - resolution: "resolve@npm:1.22.12" - dependencies: - es-errors: "npm:^1.3.0" - is-core-module: "npm:^2.16.1" - path-parse: "npm:^1.0.7" - supports-preserve-symlinks-flag: "npm:^1.0.0" - bin: - resolve: bin/resolve - checksum: 10c0/b16dc9b537c02e8c3388f7d3dcff9741d3071625f9a97ac1c885f2b0ca51e78df22328fb6d6ef214dd9101fb7cfc19aa2836fe3410402a94f3f7b8639c7149bf - languageName: node - linkType: hard - -"resolve@patch:resolve@npm%3A^1.19.0#optional!builtin": - version: 1.22.12 - resolution: "resolve@patch:resolve@npm%3A1.22.12#optional!builtin::version=1.22.12&hash=c3c19d" - dependencies: - es-errors: "npm:^1.3.0" - is-core-module: "npm:^2.16.1" - path-parse: "npm:^1.0.7" - supports-preserve-symlinks-flag: "npm:^1.0.0" - bin: - resolve: bin/resolve - checksum: 10c0/fc6519984ae1f894d877c0060ba8b1f5ba3bc0e85a02f74e141929c118c23d74d9735619a9cc2965397387e514884245c65d72a40731dcb6cfc84c7bcdc8321e - languageName: node - linkType: hard - -"restore-cursor@npm:^4.0.0": - version: 4.0.0 - resolution: "restore-cursor@npm:4.0.0" - dependencies: - onetime: "npm:^5.1.0" - signal-exit: "npm:^3.0.2" - checksum: 10c0/6f7da8c5e422ac26aa38354870b1afac09963572cf2879443540449068cb43476e9cbccf6f8de3e0171e0d6f7f533c2bc1a0a008003c9a525bbc098e89041318 - languageName: node - linkType: hard - -"rolldown@npm:1.0.3": - version: 1.0.3 - resolution: "rolldown@npm:1.0.3" - dependencies: - "@oxc-project/types": "npm:=0.133.0" - "@rolldown/binding-android-arm64": "npm:1.0.3" - "@rolldown/binding-darwin-arm64": "npm:1.0.3" - "@rolldown/binding-darwin-x64": "npm:1.0.3" - "@rolldown/binding-freebsd-x64": "npm:1.0.3" - "@rolldown/binding-linux-arm-gnueabihf": "npm:1.0.3" - "@rolldown/binding-linux-arm64-gnu": "npm:1.0.3" - "@rolldown/binding-linux-arm64-musl": "npm:1.0.3" - "@rolldown/binding-linux-ppc64-gnu": "npm:1.0.3" - "@rolldown/binding-linux-s390x-gnu": "npm:1.0.3" - "@rolldown/binding-linux-x64-gnu": "npm:1.0.3" - "@rolldown/binding-linux-x64-musl": "npm:1.0.3" - "@rolldown/binding-openharmony-arm64": "npm:1.0.3" - "@rolldown/binding-wasm32-wasi": "npm:1.0.3" - "@rolldown/binding-win32-arm64-msvc": "npm:1.0.3" - "@rolldown/binding-win32-x64-msvc": "npm:1.0.3" - "@rolldown/pluginutils": "npm:^1.0.0" - dependenciesMeta: - "@rolldown/binding-android-arm64": - optional: true - "@rolldown/binding-darwin-arm64": - optional: true - "@rolldown/binding-darwin-x64": - optional: true - "@rolldown/binding-freebsd-x64": - optional: true - "@rolldown/binding-linux-arm-gnueabihf": - optional: true - "@rolldown/binding-linux-arm64-gnu": - optional: true - "@rolldown/binding-linux-arm64-musl": - optional: true - "@rolldown/binding-linux-ppc64-gnu": - optional: true - "@rolldown/binding-linux-s390x-gnu": - optional: true - "@rolldown/binding-linux-x64-gnu": - optional: true - "@rolldown/binding-linux-x64-musl": - optional: true - "@rolldown/binding-openharmony-arm64": - optional: true - "@rolldown/binding-wasm32-wasi": - optional: true - "@rolldown/binding-win32-arm64-msvc": - optional: true - "@rolldown/binding-win32-x64-msvc": - optional: true - bin: - rolldown: ./bin/cli.mjs - checksum: 10c0/5f9dd47b7abf203b16bc600db68542f245e974c800e59ff50b76157d1dada1403657690435b036fabca88e93d13a67c31abe5cfaa6f61ce33717f61720204cdf - languageName: node - linkType: hard - -"run-async@npm:^4.0.6": - version: 4.0.6 - resolution: "run-async@npm:4.0.6" - checksum: 10c0/3e512c689d356238a06a59839deddeb09aec23bc66f780fe970fcf12b64bfc00c6880e9530ea22b8cf88a927145561f5a43343d8be87166e849ec0daaa3d4cf4 - languageName: node - linkType: hard - -"safer-buffer@npm:>= 2.1.2 < 3.0.0": - version: 2.1.2 - resolution: "safer-buffer@npm:2.1.2" - checksum: 10c0/7e3c8b2e88a1841c9671094bbaeebd94448111dd90a81a1f606f3f67708a6ec57763b3b47f06da09fc6054193e0e6709e77325415dc8422b04497a8070fa02d4 - languageName: node - linkType: hard - -"scheduler@npm:^0.27.0": - version: 0.27.0 - resolution: "scheduler@npm:0.27.0" - checksum: 10c0/4f03048cb05a3c8fddc45813052251eca00688f413a3cee236d984a161da28db28ba71bd11e7a3dd02f7af84ab28d39fb311431d3b3772fed557945beb00c452 - languageName: node - linkType: hard - -"semver@npm:7.6.3": - version: 7.6.3 - resolution: "semver@npm:7.6.3" - bin: - semver: bin/semver.js - checksum: 10c0/88f33e148b210c153873cb08cfe1e281d518aaa9a666d4d148add6560db5cd3c582f3a08ccb91f38d5f379ead256da9931234ed122057f40bb5766e65e58adaf - languageName: node - linkType: hard - -"semver@npm:^7.3.5": - version: 7.8.4 - resolution: "semver@npm:7.8.4" - bin: - semver: bin/semver.js - checksum: 10c0/81b7c296fd7927b80f67fa516b75fa1017caac8167795320de28e76ccbc6f7f01763c30ecd10d6a0d8fd089708ab0548a5aebb94b0870e99c2a2b4600a46389b - languageName: node - linkType: hard - -"siginfo@npm:^2.0.0": - version: 2.0.0 - resolution: "siginfo@npm:2.0.0" - checksum: 10c0/3def8f8e516fbb34cb6ae415b07ccc5d9c018d85b4b8611e3dc6f8be6d1899f693a4382913c9ed51a06babb5201639d76453ab297d1c54a456544acf5c892e34 - languageName: node - linkType: hard - -"signal-exit@npm:^3.0.2, signal-exit@npm:^3.0.7": - version: 3.0.7 - resolution: "signal-exit@npm:3.0.7" - checksum: 10c0/25d272fa73e146048565e08f3309d5b942c1979a6f4a58a8c59d5fa299728e9c2fcd1a759ec870863b1fd38653670240cd420dad2ad9330c71f36608a6a1c912 - languageName: node - linkType: hard - -"signal-exit@npm:^4.1.0": - version: 4.1.0 - resolution: "signal-exit@npm:4.1.0" - checksum: 10c0/41602dce540e46d599edba9d9860193398d135f7ff72cab629db5171516cfae628d21e7bfccde1bbfdf11c48726bc2a6d1a8fb8701125852fbfda7cf19c6aa83 - languageName: node - linkType: hard - -"slice-ansi@npm:^8.0.0": - version: 8.0.0 - resolution: "slice-ansi@npm:8.0.0" - dependencies: - ansi-styles: "npm:^6.2.3" - is-fullwidth-code-point: "npm:^5.1.0" - checksum: 10c0/0ce4aa91febb7cea4a00c2c27bb820fa53b6d2862ce0f80f7120134719f7914fc416b0ed966cf35250a3169e152916392f35917a2d7cad0fcc5d8b841010fa9a - languageName: node - linkType: hard - -"source-map-js@npm:^1.2.1": - version: 1.2.1 - resolution: "source-map-js@npm:1.2.1" - checksum: 10c0/7bda1fc4c197e3c6ff17de1b8b2c20e60af81b63a52cb32ec5a5d67a20a7d42651e2cb34ebe93833c5a2a084377e17455854fee3e21e7925c64a51b6a52b0faf - languageName: node - linkType: hard - -"source-map@npm:^0.5.7": - version: 0.5.7 - resolution: "source-map@npm:0.5.7" - checksum: 10c0/904e767bb9c494929be013017380cbba013637da1b28e5943b566031e29df04fba57edf3f093e0914be094648b577372bd8ad247fa98cfba9c600794cd16b599 - languageName: node - linkType: hard - -"stack-utils@npm:^2.0.6": - version: 2.0.6 - resolution: "stack-utils@npm:2.0.6" - dependencies: - escape-string-regexp: "npm:^2.0.0" - checksum: 10c0/651c9f87667e077584bbe848acaecc6049bc71979f1e9a46c7b920cad4431c388df0f51b8ad7cfd6eed3db97a2878d0fc8b3122979439ea8bac29c61c95eec8a - languageName: node - linkType: hard - -"stackback@npm:0.0.2": - version: 0.0.2 - resolution: "stackback@npm:0.0.2" - checksum: 10c0/89a1416668f950236dd5ac9f9a6b2588e1b9b62b1b6ad8dff1bfc5d1a15dbf0aafc9b52d2226d00c28dffff212da464eaeebfc6b7578b9d180cef3e3782c5983 - languageName: node - linkType: hard - -"std-env@npm:^4.0.0-rc.1": - version: 4.1.0 - resolution: "std-env@npm:4.1.0" - checksum: 10c0/2e14b6b490db34cb969a48d9cf7c35bca4a47653914aac2814221baae7b867a5b15940d133625c391621971f98cd2266a5dc7036669960e883f1081db2a56558 - languageName: node - linkType: hard - -"string-width@npm:^7.0.0": - version: 7.2.0 - resolution: "string-width@npm:7.2.0" - dependencies: - emoji-regex: "npm:^10.3.0" - get-east-asian-width: "npm:^1.0.0" - strip-ansi: "npm:^7.1.0" - checksum: 10c0/eb0430dd43f3199c7a46dcbf7a0b34539c76fe3aa62763d0b0655acdcbdf360b3f66f3d58ca25ba0205f42ea3491fa00f09426d3b7d3040e506878fc7664c9b9 - languageName: node - linkType: hard - -"string-width@npm:^8.1.0, string-width@npm:^8.1.1, string-width@npm:^8.2.0": - version: 8.2.1 - resolution: "string-width@npm:8.2.1" - dependencies: - get-east-asian-width: "npm:^1.5.0" - strip-ansi: "npm:^7.1.2" - checksum: 10c0/d467b4eaf4c40a01bb438a2620e77badd2456ffd5131c9973abe4f3acf7c802d5b21f3b6a00a5e33a7fc28ca8f9c103226e01bac61e9f259659c6f46d78e353a - languageName: node - linkType: hard - -"strip-ansi@npm:^7.1.0, strip-ansi@npm:^7.1.2": - version: 7.2.0 - resolution: "strip-ansi@npm:7.2.0" - dependencies: - ansi-regex: "npm:^6.2.2" - checksum: 10c0/544d13b7582f8254811ea97db202f519e189e59d35740c46095897e254e4f1aa9fe1524a83ad6bc5ad67d4dd6c0281d2e0219ed62b880a6238a16a17d375f221 - languageName: node - linkType: hard - -"stylis@npm:4.2.0": - version: 4.2.0 - resolution: "stylis@npm:4.2.0" - checksum: 10c0/a7128ad5a8ed72652c6eba46bed4f416521bc9745a460ef5741edc725252cebf36ee45e33a8615a7057403c93df0866ab9ee955960792db210bb80abd5ac6543 - languageName: node - linkType: hard - -"subscriptions-transport-ws@npm:^0.9.16": - version: 0.9.19 - resolution: "subscriptions-transport-ws@npm:0.9.19" - dependencies: - backo2: "npm:^1.0.2" - eventemitter3: "npm:^3.1.0" - iterall: "npm:^1.2.1" - symbol-observable: "npm:^1.0.4" - ws: "npm:^5.2.0 || ^6.0.0 || ^7.0.0" - peerDependencies: - graphql: ">=0.10.0" - checksum: 10c0/6f2ade56865f0ba291d3ff82c79781b051c2374873bac853286fedfdbc05001b8c4018ab7cba44af667ead7f573e48d18892d58a8f9ca8d90dfb4bff5c125045 - languageName: node - linkType: hard - -"supports-preserve-symlinks-flag@npm:^1.0.0": - version: 1.0.0 - resolution: "supports-preserve-symlinks-flag@npm:1.0.0" - checksum: 10c0/6c4032340701a9950865f7ae8ef38578d8d7053f5e10518076e6554a9381fa91bd9c6850193695c141f32b21f979c985db07265a758867bac95de05f7d8aeb39 - languageName: node - linkType: hard - -"symbol-observable@npm:^1.0.4": - version: 1.2.0 - resolution: "symbol-observable@npm:1.2.0" - checksum: 10c0/009fee50798ef80ed4b8195048288f108b03de162db07493f2e1fd993b33fafa72d659e832b584da5a2427daa78e5a738fb2a9ab027ee9454252e0bedbcd1fdc - languageName: node - linkType: hard - -"tagged-tag@npm:^1.0.0": - version: 1.0.0 - resolution: "tagged-tag@npm:1.0.0" - checksum: 10c0/91d25c9ffb86a91f20522cefb2cbec9b64caa1febe27ad0df52f08993ff60888022d771e868e6416cf2e72dab68449d2139e8709ba009b74c6c7ecd4000048d1 - languageName: node - linkType: hard - -"tar@npm:^7.5.4": - version: 7.5.16 - resolution: "tar@npm:7.5.16" - dependencies: - "@isaacs/fs-minipass": "npm:^4.0.0" - chownr: "npm:^3.0.0" - minipass: "npm:^7.1.2" - minizlib: "npm:^3.1.0" - yallist: "npm:^5.0.0" - checksum: 10c0/4f37f3c4bd2ca2755fd736a5df1d573c1a868ec1b1e893346aeafa95ac510f9e2fd1469420bd866cc7904799e5bd4ac62b5d4f03fe27747d6e1e373b44505c5c - languageName: node - linkType: hard - -"terminal-size@npm:^4.0.1": - version: 4.0.1 - resolution: "terminal-size@npm:4.0.1" - checksum: 10c0/89afd9d816dd9dbfe4499da9aeea70491bbde4ff4592226a9c8ac71074a7580afead6a78e95ecc35f6d42e09087b55ffcb1019302cd55e0cc957b6ce5c4847e8 - languageName: node - linkType: hard - -"tinybench@npm:^2.9.0": - version: 2.9.0 - resolution: "tinybench@npm:2.9.0" - checksum: 10c0/c3500b0f60d2eb8db65250afe750b66d51623057ee88720b7f064894a6cb7eb93360ca824a60a31ab16dab30c7b1f06efe0795b352e37914a9d4bad86386a20c - languageName: node - linkType: hard - -"tinyexec@npm:^1.0.2": - version: 1.2.4 - resolution: "tinyexec@npm:1.2.4" - checksum: 10c0/153b8db6b080194b558ff145b9cffc36b80a6e07babd644dcfbe49c807eee668c876049d28bdee90b96304476f883352f2dad91b3f86bc23832532f4363e66ff - languageName: node - linkType: hard - -"tinyglobby@npm:^0.2.12, tinyglobby@npm:^0.2.15, tinyglobby@npm:^0.2.17": - version: 0.2.17 - resolution: "tinyglobby@npm:0.2.17" - dependencies: - fdir: "npm:^6.5.0" - picomatch: "npm:^4.0.4" - checksum: 10c0/7f7bb0f197c88bc4b20c231e0deca4240ca3bf313a88f5a7fee93a872b84966a4d50220947c0455ad07a60b3b360961c5b7fd979222aeb716a9f99b412002e4c - languageName: node - linkType: hard - -"tinyrainbow@npm:^3.1.0": - version: 3.1.0 - resolution: "tinyrainbow@npm:3.1.0" - checksum: 10c0/f11cf387a26c5c9255bec141a90ac511b26172981b10c3e50053bc6700ea7d2336edcc4a3a21dbb8412fe7c013477d2ba4d7e4877800f3f8107be5105aad6511 - languageName: node - linkType: hard - -"tr46@npm:~0.0.3": - version: 0.0.3 - resolution: "tr46@npm:0.0.3" - checksum: 10c0/047cb209a6b60c742f05c9d3ace8fa510bff609995c129a37ace03476a9b12db4dbf975e74600830ef0796e18882b2381fb5fb1f6b4f96b832c374de3ab91a11 - languageName: node - linkType: hard - -"tsconfck@npm:^3.0.3": - version: 3.1.6 - resolution: "tsconfck@npm:3.1.6" - peerDependencies: - typescript: ^5.0.0 - peerDependenciesMeta: - typescript: - optional: true - bin: - tsconfck: bin/tsconfck.js - checksum: 10c0/269c3c513540be44844117bb9b9258fe6f8aeab026d32aeebf458d5299125f330711429dbb556dbf125a0bc25f4a81e6c24ac96de2740badd295c3fb400f66c4 - languageName: node - linkType: hard - -"tslib@npm:^1.9.3": - version: 1.14.1 - resolution: "tslib@npm:1.14.1" - checksum: 10c0/69ae09c49eea644bc5ebe1bca4fa4cc2c82b7b3e02f43b84bd891504edf66dbc6b2ec0eef31a957042de2269139e4acff911e6d186a258fb14069cd7f6febce2 - languageName: node - linkType: hard - -"tslib@npm:^2.0.0, tslib@npm:^2.4.0": - version: 2.8.1 - resolution: "tslib@npm:2.8.1" - checksum: 10c0/9c4759110a19c53f992d9aae23aac5ced636e99887b51b9e61def52611732872ff7668757d4e4c61f19691e36f4da981cd9485e869b4a7408d689f6bf1f14e62 - languageName: node - linkType: hard - -"twenty-client-sdk@npm:2.15.0": - version: 2.15.0 - resolution: "twenty-client-sdk@npm:2.15.0" - dependencies: - "@genql/runtime": "npm:^2.10.0" - esbuild: "npm:^0.28.1" - graphql: "npm:^16.8.1" - lodash: "npm:^4.17.21" - prettier: "npm:^3.8.3" - checksum: 10c0/ea5143511ec3d42a0c2eaabda8833bac5c73c25fe04ba967d76b47b22c67468dd59d9e748a00e99a8085ffdb02d6cc5f41556f9e85a9785b2909b63adcb4205f - languageName: node - linkType: hard - -"twenty-meeting-bot@workspace:.": - version: 0.0.0-use.local - resolution: "twenty-meeting-bot@workspace:." - dependencies: - "@emotion/react": "npm:^11.14.0" - "@emotion/styled": "npm:^11.14.0" - "@sniptt/guards": "npm:^0.2.0" - "@types/node": "npm:^24.7.2" - "@types/react": "npm:^19.0.0" - "@typescript/native-preview": "npm:^7.0.0-dev.20260116.1" - oxlint: "npm:^0.16.0" - react: "npm:^19.0.0" - react-dom: "npm:^19.0.0" - twenty-client-sdk: "npm:2.15.0" - twenty-sdk: "npm:2.15.0" - typescript: "npm:^5.9.3" - vite-tsconfig-paths: "npm:^4.2.1" - vitest: "npm:^4.1.9" - languageName: unknown - linkType: soft - -"twenty-sdk@npm:2.15.0": - version: 2.15.0 - resolution: "twenty-sdk@npm:2.15.0" - dependencies: - "@sniptt/guards": "npm:^0.2.0" - axios: "npm:^1.16.0" - chalk: "npm:^5.3.0" - chokidar: "npm:^4.0.0" - commander: "npm:^12.0.0" - esbuild: "npm:^0.28.1" - graphql: "npm:^16.8.1" - graphql-sse: "npm:^2.5.4" - ink: "npm:^6.8.0" - inquirer: "npm:^14.0.0" - jsonc-parser: "npm:^3.2.0" - preact: "npm:^10.28.3" - react: "npm:^19.2.0" - react-dom: "npm:^19.2.0" - semver: "npm:7.6.3" - tinyglobby: "npm:^0.2.15" - twenty-client-sdk: "npm:2.15.0" - typescript: "npm:^5.9.3" - uuid: "npm:^13.0.2" - bin: - twenty: dist/cli.cjs - checksum: 10c0/0fe9a3653f3adaa54eb8398e520328ed2fd51b4c4284b4ed4105dfd92e3c866f2e9b5c6933f952e974684cd38169cdb26d64cfe06f058f4f4cb5bb1f6be7d2fc - languageName: node - linkType: hard - -"type-fest@npm:^5.4.1": - version: 5.7.0 - resolution: "type-fest@npm:5.7.0" - dependencies: - tagged-tag: "npm:^1.0.0" - checksum: 10c0/f71ed17b753649421e419db8cc2e140f930333a1467b1d9cca2e0e4052900fd442f2360bae73f3a6bf9340d949ac46d9a1598c709b4c8089272e7624df9c8716 - languageName: node - linkType: hard - -"typescript@npm:^5.9.3": - version: 5.9.3 - resolution: "typescript@npm:5.9.3" - bin: - tsc: bin/tsc - tsserver: bin/tsserver - checksum: 10c0/6bd7552ce39f97e711db5aa048f6f9995b53f1c52f7d8667c1abdc1700c68a76a308f579cd309ce6b53646deb4e9a1be7c813a93baaf0a28ccd536a30270e1c5 - languageName: node - linkType: hard - -"typescript@patch:typescript@npm%3A^5.9.3#optional!builtin": - version: 5.9.3 - resolution: "typescript@patch:typescript@npm%3A5.9.3#optional!builtin::version=5.9.3&hash=5786d5" - bin: - tsc: bin/tsc - tsserver: bin/tsserver - checksum: 10c0/ad09fdf7a756814dce65bc60c1657b40d44451346858eea230e10f2e95a289d9183b6e32e5c11e95acc0ccc214b4f36289dcad4bf1886b0adb84d711d336a430 - languageName: node - linkType: hard - -"undici-types@npm:>=7.24.0 <7.24.7": - version: 7.24.6 - resolution: "undici-types@npm:7.24.6" - checksum: 10c0/d9cd8befb643ac904615c280a095ba4240531f6bb4a5e75a22a7483630ca8d3f1016d2ab6ace6ceda1f63b3a2db2fe037fafe121d6917a0187573aa548ff78ca - languageName: node - linkType: hard - -"undici-types@npm:~7.18.0": - version: 7.18.2 - resolution: "undici-types@npm:7.18.2" - checksum: 10c0/85a79189113a238959d7a647368e4f7c5559c3a404ebdb8fc4488145ce9426fcd82252a844a302798dfc0e37e6fb178ff481ed03bc4caf634c5757d9ef43521d - languageName: node - linkType: hard - -"undici@npm:^6.25.0": - version: 6.27.0 - resolution: "undici@npm:6.27.0" - checksum: 10c0/f88c3dae3957dbf9d93cb481440aced317bd3c4941b5914fea5efba516d51138988cdb5c76006f0bb1337e41d56c3443351055d492e73af2428521c37ba2a76f - languageName: node - linkType: hard - -"unfetch@npm:^4.2.0": - version: 4.2.0 - resolution: "unfetch@npm:4.2.0" - checksum: 10c0/a5c0a896a6f09f278b868075aea65652ad185db30e827cb7df45826fe5ab850124bf9c44c4dafca4bf0c55a0844b17031e8243467fcc38dd7a7d435007151f1b - languageName: node - linkType: hard - -"utility-types@npm:^3.10.0": - version: 3.11.0 - resolution: "utility-types@npm:3.11.0" - checksum: 10c0/2f1580137b0c3e6cf5405f37aaa8f5249961a76d26f1ca8efc0ff49a2fc0e0b2db56de8e521a174d075758e0c7eb3e590edec0832eb44478b958f09914920f19 - languageName: node - linkType: hard - -"uuid@npm:^13.0.2": - version: 13.0.2 - resolution: "uuid@npm:13.0.2" - bin: - uuid: dist-node/bin/uuid - checksum: 10c0/32c7ee84fa7c7966cc09b3a1514a752a8e2f609f15c00033fbaedc0255d577d1877b839f11ee537f37e587bbc620bbb98c3b3a1482931b8ed47d79497cd7a7cd - languageName: node - linkType: hard - -"vite-tsconfig-paths@npm:^4.2.1": - version: 4.3.2 - resolution: "vite-tsconfig-paths@npm:4.3.2" - dependencies: - debug: "npm:^4.1.1" - globrex: "npm:^0.1.2" - tsconfck: "npm:^3.0.3" - peerDependencies: - vite: "*" - peerDependenciesMeta: - vite: - optional: true - checksum: 10c0/f390ac1d1c3992fc5ac50f9274c1090f8b55ab34a89ea88893db9a6924a3b26c9f64bc1163615150ad100749db73b6b2cf1d57f6cd60df6e762ceb5b8ad30024 - languageName: node - linkType: hard - -"vite@npm:^6.0.0 || ^7.0.0 || ^8.0.0": - version: 8.0.16 - resolution: "vite@npm:8.0.16" - dependencies: - fsevents: "npm:~2.3.3" - lightningcss: "npm:^1.32.0" - picomatch: "npm:^4.0.4" - postcss: "npm:^8.5.15" - rolldown: "npm:1.0.3" - tinyglobby: "npm:^0.2.17" - peerDependencies: - "@types/node": ^20.19.0 || >=22.12.0 - "@vitejs/devtools": ^0.1.18 - esbuild: ^0.27.0 || ^0.28.0 - jiti: ">=1.21.0" - less: ^4.0.0 - sass: ^1.70.0 - sass-embedded: ^1.70.0 - stylus: ">=0.54.8" - sugarss: ^5.0.0 - terser: ^5.16.0 - tsx: ^4.8.1 - yaml: ^2.4.2 - dependenciesMeta: - fsevents: - optional: true - peerDependenciesMeta: - "@types/node": - optional: true - "@vitejs/devtools": - optional: true - esbuild: - optional: true - jiti: - optional: true - less: - optional: true - sass: - optional: true - sass-embedded: - optional: true - stylus: - optional: true - sugarss: - optional: true - terser: - optional: true - tsx: - optional: true - yaml: - optional: true - bin: - vite: bin/vite.js - checksum: 10c0/d75be3fbe2f63e6a8145325970338afaf0dd4d96ba9175c13f9a286fd5f95afc489401b693e4fa6c0899a4dd0e137be91cdf9401a40a635563911ad5036e3467 - languageName: node - linkType: hard - -"vitest@npm:^4.1.9": - version: 4.1.9 - resolution: "vitest@npm:4.1.9" - dependencies: - "@vitest/expect": "npm:4.1.9" - "@vitest/mocker": "npm:4.1.9" - "@vitest/pretty-format": "npm:4.1.9" - "@vitest/runner": "npm:4.1.9" - "@vitest/snapshot": "npm:4.1.9" - "@vitest/spy": "npm:4.1.9" - "@vitest/utils": "npm:4.1.9" - es-module-lexer: "npm:^2.0.0" - expect-type: "npm:^1.3.0" - magic-string: "npm:^0.30.21" - obug: "npm:^2.1.1" - pathe: "npm:^2.0.3" - picomatch: "npm:^4.0.3" - std-env: "npm:^4.0.0-rc.1" - tinybench: "npm:^2.9.0" - tinyexec: "npm:^1.0.2" - tinyglobby: "npm:^0.2.15" - tinyrainbow: "npm:^3.1.0" - vite: "npm:^6.0.0 || ^7.0.0 || ^8.0.0" - why-is-node-running: "npm:^2.3.0" - peerDependencies: - "@edge-runtime/vm": "*" - "@opentelemetry/api": ^1.9.0 - "@types/node": ^20.0.0 || ^22.0.0 || >=24.0.0 - "@vitest/browser-playwright": 4.1.9 - "@vitest/browser-preview": 4.1.9 - "@vitest/browser-webdriverio": 4.1.9 - "@vitest/coverage-istanbul": 4.1.9 - "@vitest/coverage-v8": 4.1.9 - "@vitest/ui": 4.1.9 - happy-dom: "*" - jsdom: "*" - vite: ^6.0.0 || ^7.0.0 || ^8.0.0 - peerDependenciesMeta: - "@edge-runtime/vm": - optional: true - "@opentelemetry/api": - optional: true - "@types/node": - optional: true - "@vitest/browser-playwright": - optional: true - "@vitest/browser-preview": - optional: true - "@vitest/browser-webdriverio": - optional: true - "@vitest/coverage-istanbul": - optional: true - "@vitest/coverage-v8": - optional: true - "@vitest/ui": - optional: true - happy-dom: - optional: true - jsdom: - optional: true - vite: - optional: false - bin: - vitest: ./vitest.mjs - checksum: 10c0/1ac80ef4991be82822a52aea48415f1bc64ddf8fd88ee24c172ec368f1d480fefacbde622c3c951982f7961a1d07313e18deaafc774d29e42ad6f6ffa63334a7 - languageName: node - linkType: hard - -"webidl-conversions@npm:^3.0.0": - version: 3.0.1 - resolution: "webidl-conversions@npm:3.0.1" - checksum: 10c0/5612d5f3e54760a797052eb4927f0ddc01383550f542ccd33d5238cfd65aeed392a45ad38364970d0a0f4fea32e1f4d231b3d8dac4a3bdd385e5cf802ae097db - languageName: node - linkType: hard - -"whatwg-url@npm:^5.0.0": - version: 5.0.0 - resolution: "whatwg-url@npm:5.0.0" - dependencies: - tr46: "npm:~0.0.3" - webidl-conversions: "npm:^3.0.0" - checksum: 10c0/1588bed84d10b72d5eec1d0faa0722ba1962f1821e7539c535558fb5398d223b0c50d8acab950b8c488b4ba69043fd833cc2697056b167d8ad46fac3995a55d5 - languageName: node - linkType: hard - -"which@npm:^6.0.0": - version: 6.0.1 - resolution: "which@npm:6.0.1" - dependencies: - isexe: "npm:^4.0.0" - bin: - node-which: bin/which.js - checksum: 10c0/7e710e54ea36d2d6183bee2f9caa27a3b47b9baf8dee55a199b736fcf85eab3b9df7556fca3d02b50af7f3dfba5ea3a45644189836df06267df457e354da66d5 - languageName: node - linkType: hard - -"why-is-node-running@npm:^2.3.0": - version: 2.3.0 - resolution: "why-is-node-running@npm:2.3.0" - dependencies: - siginfo: "npm:^2.0.0" - stackback: "npm:0.0.2" - bin: - why-is-node-running: cli.js - checksum: 10c0/1cde0b01b827d2cf4cb11db962f3958b9175d5d9e7ac7361d1a7b0e2dc6069a263e69118bd974c4f6d0a890ef4eedfe34cf3d5167ec14203dbc9a18620537054 - languageName: node - linkType: hard - -"widest-line@npm:^6.0.0": - version: 6.0.0 - resolution: "widest-line@npm:6.0.0" - dependencies: - string-width: "npm:^8.1.0" - checksum: 10c0/735f1fdcd97fe765a07bb8b5e73c020bed8e53ab34e83ce0ef01693ba3c914d9e7977fe5f5facf0d0b670297a82dd5e376d3efa0896860dfcdaf7cd6924c0fb7 - languageName: node - linkType: hard - -"wrap-ansi@npm:^9.0.0": - version: 9.0.2 - resolution: "wrap-ansi@npm:9.0.2" - dependencies: - ansi-styles: "npm:^6.2.1" - string-width: "npm:^7.0.0" - strip-ansi: "npm:^7.1.0" - checksum: 10c0/3305839b9a0d6fb930cb63a52f34d3936013d8b0682ff3ec133c9826512620f213800ffa19ea22904876d5b7e9a3c1f40682f03597d986a4ca881fa7b033688c - languageName: node - linkType: hard - -"ws@npm:^5.2.0 || ^6.0.0 || ^7.0.0": - version: 7.5.11 - resolution: "ws@npm:7.5.11" - peerDependencies: - bufferutil: ^4.0.1 - utf-8-validate: ^5.0.2 - peerDependenciesMeta: - bufferutil: - optional: true - utf-8-validate: - optional: true - checksum: 10c0/7972670b676fb1ccba73b0899ca3c2e04e8c2075629c2614cced7f556536f96a672bbf4619fc5a06c8b8720bb839a47ca88c69c95dc14c9c61a99fbecba1c866 - languageName: node - linkType: hard - -"ws@npm:^6.1.4": - version: 6.2.4 - resolution: "ws@npm:6.2.4" - dependencies: - async-limiter: "npm:~1.0.0" - checksum: 10c0/5c2b9474164f9cb68c7776a1d10b0461c186f3a69bffb1028fca33eba5ab7206a09173fb0b311d6c5a81c8cf148406f8deb0b7d899542ab8ca67407d99717dad - languageName: node - linkType: hard - -"ws@npm:^8.18.0": - version: 8.21.0 - resolution: "ws@npm:8.21.0" - peerDependencies: - bufferutil: ^4.0.1 - utf-8-validate: ">=5.0.2" - peerDependenciesMeta: - bufferutil: - optional: true - utf-8-validate: - optional: true - checksum: 10c0/ef4a243476283fc49bc7550966c4af4aa0eef56273837211e700de3b664e08604a760cdddcb5ba43c049140e74ccfec5b0ee0bb439e08c2adf9138902fdde5f9 - languageName: node - linkType: hard - -"yallist@npm:^5.0.0": - version: 5.0.0 - resolution: "yallist@npm:5.0.0" - checksum: 10c0/a499c81ce6d4a1d260d4ea0f6d49ab4da09681e32c3f0472dee16667ed69d01dae63a3b81745a24bd78476ec4fcf856114cb4896ace738e01da34b2c42235416 - languageName: node - linkType: hard - -"yaml@npm:^1.10.0": - version: 1.10.3 - resolution: "yaml@npm:1.10.3" - checksum: 10c0/c309ff85a0a569a981d71ab9cf0fef68672a16b9cdf40639d1c3b30034f6cd16ee428602bd6d64ecf006f8c8bee499023cac236538f79898aa99fb5db529a2ed - languageName: node - linkType: hard - -"yoga-layout@npm:~3.2.1": - version: 3.2.1 - resolution: "yoga-layout@npm:3.2.1" - checksum: 10c0/9001e51be993c85e03757e5a04a2b61b8b30c9e5a7865d0156ca87a6431a3b717d51eb4990bfe588189fcfeac688dd9c3de707bbd50d1c344a84e63974cc54a8 - languageName: node - linkType: hard - -"zen-observable-ts@npm:^0.8.21": - version: 0.8.21 - resolution: "zen-observable-ts@npm:0.8.21" - dependencies: - tslib: "npm:^1.9.3" - zen-observable: "npm:^0.8.0" - checksum: 10c0/fe4a02f862b5f7e8ae0f86230c37b84c7d5611f5c206981afb4043e732d04cf7067a6cbe1ba82d20f18b735a3387937195a12542158a631d308ae3959a1d93c4 - languageName: node - linkType: hard - -"zen-observable@npm:^0.8.0": - version: 0.8.15 - resolution: "zen-observable@npm:0.8.15" - checksum: 10c0/71cc2f2bbb537300c3f569e25693d37b3bc91f225cefce251a71c30bc6bb3e7f8e9420ca0eb57f2ac9e492b085b8dfa075fd1e8195c40b83c951dd59c6e4fbf8 - languageName: node - linkType: hard