@agent-native/core 0.123.0 → 0.123.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/corpus/README.md +1 -1
- package/corpus/core/CHANGELOG.md +24 -0
- package/corpus/core/package.json +1 -1
- package/corpus/core/src/cli/templates-meta.ts +12 -0
- package/corpus/core/src/deploy/build.ts +8 -0
- package/corpus/core/src/server/builder-browser.ts +76 -0
- package/corpus/core/src/server/core-routes-plugin.ts +15 -5
- package/corpus/templates/assets/app/global.css +0 -4
- package/corpus/templates/assets/app/routes/brand-kits.$id.tsx +195 -167
- package/corpus/templates/assets/changelog/2026-07-23-drafts-preview-panel-now-fills-the-full-width-with-a-large-p.md +6 -0
- package/corpus/templates/content/actions/_builder-cms-source-adapter.ts +1 -0
- package/corpus/templates/content/actions/_builder-cms-write-client.ts +55 -9
- package/corpus/templates/content/actions/_database-source-utils.ts +756 -182
- package/corpus/templates/content/actions/change-content-database-source-role.ts +21 -20
- package/corpus/templates/content/actions/execute-builder-source-execution.ts +49 -8
- package/corpus/templates/content/actions/materialize-builder-required-fields.ts +42 -9
- package/corpus/templates/content/actions/prepare-builder-source-review.ts +88 -90
- package/corpus/templates/content/actions/preview-builder-source-review.ts +4 -3
- package/corpus/templates/content/actions/refresh-content-database-source.ts +36 -9
- package/corpus/templates/content/actions/set-content-database-source-write-mode.ts +28 -24
- package/corpus/templates/content/app/components/editor/BubbleToolbar.tsx +62 -12
- package/corpus/templates/content/app/components/editor/DocumentEditor.tsx +91 -16
- package/corpus/templates/content/app/components/editor/DocumentProperties.tsx +15 -1
- package/corpus/templates/content/app/components/editor/SlashCommandMenu.tsx +234 -44
- package/corpus/templates/content/app/components/editor/VisualEditor.tsx +194 -16
- package/corpus/templates/content/app/components/editor/database/DatabaseView.tsx +95 -42
- package/corpus/templates/content/app/components/editor/database-sources/BuilderSourceReviewDialog.tsx +4 -2
- package/corpus/templates/content/app/components/editor/extensions/CodeBlockNode.tsx +10 -166
- package/corpus/templates/content/app/components/editor/extensions/VideoBlock.tsx +32 -8
- package/corpus/templates/content/app/components/editor/extensions/VideoNode.ts +14 -0
- package/corpus/templates/content/app/components/editor/registrySlashItems.ts +5 -1
- package/corpus/templates/content/app/hooks/use-content-database.ts +59 -2
- package/corpus/templates/content/app/hooks/use-documents.ts +18 -26
- package/corpus/templates/content/app/root.tsx +1 -6
- package/corpus/templates/content/changelog/2026-07-24-builder-can-now-publish-approved-updates-to-entries-that-are.md +6 -0
- package/corpus/templates/content/changelog/2026-07-24-builder-collection-loading-no-longer-restarts-from-the-begin.md +6 -0
- package/corpus/templates/content/changelog/2026-07-24-builder-connections-now-complete-securely-from-netlify-deplo.md +6 -0
- package/corpus/templates/content/changelog/2026-07-24-builder-draft-creation-now-scopes-execution-to-the-selected-.md +6 -0
- package/corpus/templates/content/changelog/2026-07-24-builder-review-now-distinguishes-preparing-a-safe-dry-run-from.md +6 -0
- package/corpus/templates/content/changelog/2026-07-24-builder-source-imports-now-recover-from-a-transient-paginati.md +6 -0
- package/corpus/templates/content/changelog/2026-07-24-builder-source-refresh-now-preserves-the-provider-bound-fiel.md +6 -0
- package/corpus/templates/content/changelog/2026-07-24-builder-update-reviews-now-open-reliably-on-large-collection.md +6 -0
- package/corpus/templates/content/changelog/2026-07-24-builder-write-policy-controls-now-reflect-successful-changes.md +6 -0
- package/corpus/templates/content/changelog/2026-07-24-date-properties-now-save-reliably-when-selected-with-the-nat.md +6 -0
- package/corpus/templates/content/changelog/2026-07-24-editor-slash-search-now-keeps-multi-word-block-names-open.md +6 -0
- package/corpus/templates/content/changelog/2026-07-24-exact-editor-slash-commands-now-execute-on-enter-even-if-the.md +6 -0
- package/corpus/templates/content/changelog/2026-07-24-new-collaborative-pages-now-save-their-first-edits.md +6 -0
- package/corpus/templates/content/changelog/2026-07-24-new-local-builder-rows-no-longer-remain-locked-behind-a-none.md +6 -0
- package/corpus/templates/content/changelog/2026-07-24-opening-or-closing-a-collaborative-document-no-longer-lets-u.md +6 -0
- package/corpus/templates/content/changelog/2026-07-24-selected-text-now-opens-the-link-editor-with-command-k-or-co.md +6 -0
- package/corpus/templates/content/changelog/2026-07-24-slash-menu-block-choices-no-longer-stall-pointer-interaction.md +6 -0
- package/corpus/templates/content/changelog/2026-07-24-uploaded-and-embedded-media-now-stay-intact-while-collaborative-document-drafts-save.md +6 -0
- package/corpus/templates/content/changelog/2026-07-24-video-link-insertion-now-keeps-its-source-panel-open-while-t.md +6 -0
- package/corpus/templates/content/changelog/2026-07-26-builder-publishing-now-shows-safe-validation-details-instead.md +6 -0
- package/corpus/templates/content/changelog/2026-07-26-builder-reviews-stay-responsive-on-large-collections-and-pre.md +6 -0
- package/corpus/templates/content/changelog/2026-07-26-collaborative-documents-no-longer-flash-duplicated-recent-bl.md +6 -0
- package/corpus/templates/content/changelog/2026-07-26-corrected-builder-updates-are-now-reviewed-separately-from-f.md +6 -0
- package/corpus/templates/content/changelog/2026-07-26-documents-now-remain-read-only-until-their-live-collaborativ.md +6 -0
- package/corpus/templates/content/changelog/2026-07-26-slash-command-menus-now-stay-reachable-when-editing-near-the.md +6 -0
- package/corpus/templates/content/changelog/2026-07-26-the-slash-menu-now-shows-one-clear-callout-choice-instead-of.md +6 -0
- package/corpus/templates/content/shared/api.ts +1 -0
- package/corpus/templates/content/shared/builder-mdx.ts +73 -16
- package/corpus/templates/crm/.agents/skills/agent-native-docs/SKILL.md +115 -0
- package/corpus/templates/crm/.agents/skills/agent-native-toolkit/SKILL.md +177 -0
- package/corpus/templates/crm/.agents/skills/crm/SKILL.md +205 -0
- package/corpus/templates/crm/.agents/skills/customizing-agent-native/SKILL.md +220 -0
- package/corpus/templates/crm/.agents/skills/feature-flags/SKILL.md +169 -0
- package/corpus/templates/crm/.agents/skills/sharing/SKILL.md +228 -0
- package/corpus/templates/crm/.agents/skills/storing-data/SKILL.md +178 -0
- package/corpus/templates/crm/.agents/skills/upgrade-agent-native/SKILL.md +122 -0
- package/corpus/templates/crm/.env.example +5 -0
- package/corpus/templates/crm/AGENTS.md +82 -0
- package/corpus/templates/crm/CHANGELOG.md +25 -0
- package/corpus/templates/crm/DEVELOPING.md +66 -0
- package/corpus/templates/crm/README.md +122 -0
- package/corpus/templates/crm/_gitignore +12 -0
- package/corpus/templates/crm/actions/_crm-action-utils.ts +98 -0
- package/corpus/templates/crm/actions/_crm-attribute-utils.ts +219 -0
- package/corpus/templates/crm/actions/_crm-dashboard.ts +69 -0
- package/corpus/templates/crm/actions/_crm-data-program-actions.ts +15 -0
- package/corpus/templates/crm/actions/_crm-list-utils.ts +775 -0
- package/corpus/templates/crm/actions/_crm-signal-utils.ts +79 -0
- package/corpus/templates/crm/actions/add-crm-record-to-list.ts +164 -0
- package/corpus/templates/crm/actions/apply-crm-proposals.ts +291 -0
- package/corpus/templates/crm/actions/archive-crm-attribute.ts +69 -0
- package/corpus/templates/crm/actions/attach-call-evidence.ts +170 -0
- package/corpus/templates/crm/actions/configure-crm-connection.ts +126 -0
- package/corpus/templates/crm/actions/configure-native-crm.ts +44 -0
- package/corpus/templates/crm/actions/create-crm-attribute.ts +350 -0
- package/corpus/templates/crm/actions/create-crm-list.ts +194 -0
- package/corpus/templates/crm/actions/create-crm-record.ts +148 -0
- package/corpus/templates/crm/actions/create-crm-signal-tracker.ts +58 -0
- package/corpus/templates/crm/actions/delete-crm-saved-view.ts +68 -0
- package/corpus/templates/crm/actions/delete-staged-dataset.ts +14 -0
- package/corpus/templates/crm/actions/estimate-crm-enrichment.ts +229 -0
- package/corpus/templates/crm/actions/find-crm-duplicates.ts +87 -0
- package/corpus/templates/crm/actions/get-crm-automation-recipe.ts +36 -0
- package/corpus/templates/crm/actions/get-crm-dashboard-panel.ts +35 -0
- package/corpus/templates/crm/actions/get-crm-dashboard.ts +14 -0
- package/corpus/templates/crm/actions/get-crm-overview.ts +13 -0
- package/corpus/templates/crm/actions/get-crm-pipeline-data.ts +61 -0
- package/corpus/templates/crm/actions/get-crm-record-page.ts +287 -0
- package/corpus/templates/crm/actions/get-crm-record.ts +75 -0
- package/corpus/templates/crm/actions/get-crm-workspace.ts +364 -0
- package/corpus/templates/crm/actions/install-crm-pipeline-dashboard.ts +81 -0
- package/corpus/templates/crm/actions/list-crm-attributes.ts +71 -0
- package/corpus/templates/crm/actions/list-crm-connections.ts +68 -0
- package/corpus/templates/crm/actions/list-crm-dashboard-revisions.ts +15 -0
- package/corpus/templates/crm/actions/list-crm-dashboards.ts +15 -0
- package/corpus/templates/crm/actions/list-crm-enrichment-slots.ts +28 -0
- package/corpus/templates/crm/actions/list-crm-list-entries.ts +192 -0
- package/corpus/templates/crm/actions/list-crm-lists.ts +80 -0
- package/corpus/templates/crm/actions/list-crm-proposals.ts +55 -0
- package/corpus/templates/crm/actions/list-crm-record-field-history.ts +201 -0
- package/corpus/templates/crm/actions/list-crm-record-values.ts +200 -0
- package/corpus/templates/crm/actions/list-crm-records.ts +103 -0
- package/corpus/templates/crm/actions/list-crm-saved-views.ts +66 -0
- package/corpus/templates/crm/actions/list-crm-signal-hits.ts +43 -0
- package/corpus/templates/crm/actions/list-crm-signal-trackers.ts +32 -0
- package/corpus/templates/crm/actions/list-crm-tasks.ts +22 -0
- package/corpus/templates/crm/actions/list-staged-datasets.ts +14 -0
- package/corpus/templates/crm/actions/list-workspace-connections.ts +47 -0
- package/corpus/templates/crm/actions/manage-crm-attribute-option.ts +272 -0
- package/corpus/templates/crm/actions/manage-crm-signal-tracker.ts +87 -0
- package/corpus/templates/crm/actions/manage-crm-task.ts +132 -0
- package/corpus/templates/crm/actions/merge-crm-records.ts +691 -0
- package/corpus/templates/crm/actions/navigate.ts +54 -0
- package/corpus/templates/crm/actions/provider-api-catalog.ts +22 -0
- package/corpus/templates/crm/actions/provider-api-docs.ts +54 -0
- package/corpus/templates/crm/actions/provider-api-request.ts +92 -0
- package/corpus/templates/crm/actions/query-staged-dataset.ts +44 -0
- package/corpus/templates/crm/actions/record-crm-call-insight.ts +160 -0
- package/corpus/templates/crm/actions/record-crm-smart-signal.ts +150 -0
- package/corpus/templates/crm/actions/remove-crm-list-entry.ts +79 -0
- package/corpus/templates/crm/actions/restore-crm-dashboard-revision.ts +23 -0
- package/corpus/templates/crm/actions/review-crm-signal.ts +31 -0
- package/corpus/templates/crm/actions/run-crm-attribute-fill.ts +443 -0
- package/corpus/templates/crm/actions/run-crm-enrichment.ts +647 -0
- package/corpus/templates/crm/actions/run-crm-saved-view-program.ts +103 -0
- package/corpus/templates/crm/actions/run-crm-signal-trackers.ts +265 -0
- package/corpus/templates/crm/actions/run.ts +7 -0
- package/corpus/templates/crm/actions/save-crm-dashboard.ts +51 -0
- package/corpus/templates/crm/actions/save-crm-saved-view.ts +296 -0
- package/corpus/templates/crm/actions/sync-crm.ts +146 -0
- package/corpus/templates/crm/actions/update-crm-attribute.ts +111 -0
- package/corpus/templates/crm/actions/update-crm-list-entry.ts +102 -0
- package/corpus/templates/crm/actions/update-crm-list.ts +97 -0
- package/corpus/templates/crm/actions/update-crm-record.ts +543 -0
- package/corpus/templates/crm/actions/view-screen.ts +324 -0
- package/corpus/templates/crm/agent-native.app-skill.json +69 -0
- package/corpus/templates/crm/app/components/crm/CreateCrmRecordDialog.tsx +158 -0
- package/corpus/templates/crm/app/components/crm/CrmDashboardPanel.tsx +78 -0
- package/corpus/templates/crm/app/components/crm/CrmSignalsPanel.tsx +220 -0
- package/corpus/templates/crm/app/components/crm/IntelligenceSettings.tsx +414 -0
- package/corpus/templates/crm/app/components/crm/RecordActions.tsx +648 -0
- package/corpus/templates/crm/app/components/crm/RecordGrid.tsx +488 -0
- package/corpus/templates/crm/app/components/crm/RecordWorkspace.tsx +232 -0
- package/corpus/templates/crm/app/components/crm/SavedViewDataProgram.tsx +93 -0
- package/corpus/templates/crm/app/components/crm/Surface.tsx +97 -0
- package/corpus/templates/crm/app/components/crm/WorkOverview.tsx +180 -0
- package/corpus/templates/crm/app/components/crm/board/CrmBoard.tsx +988 -0
- package/corpus/templates/crm/app/components/crm/board/board-model.ts +399 -0
- package/corpus/templates/crm/app/components/crm/board/entry-filter.ts +150 -0
- package/corpus/templates/crm/app/components/crm/board/view-draft.ts +227 -0
- package/corpus/templates/crm/app/components/crm/grid/CrmGrid.tsx +1033 -0
- package/corpus/templates/crm/app/components/crm/grid/GridCell.tsx +620 -0
- package/corpus/templates/crm/app/components/crm/grid/clipboard.ts +84 -0
- package/corpus/templates/crm/app/components/crm/grid/model.ts +304 -0
- package/corpus/templates/crm/app/components/crm/grid/navigation.ts +220 -0
- package/corpus/templates/crm/app/components/crm/grid/query.ts +214 -0
- package/corpus/templates/crm/app/components/crm/record/AttributePanel.tsx +192 -0
- package/corpus/templates/crm/app/components/crm/record/FieldHistory.tsx +179 -0
- package/corpus/templates/crm/app/components/crm/record/RecordHeader.tsx +103 -0
- package/corpus/templates/crm/app/components/crm/record/RecordLists.tsx +235 -0
- package/corpus/templates/crm/app/components/crm/record/RecordTabs.tsx +206 -0
- package/corpus/templates/crm/app/components/crm/record/field-editors.tsx +245 -0
- package/corpus/templates/crm/app/components/crm/record/record-data.ts +603 -0
- package/corpus/templates/crm/app/components/crm/settings/AdvancedSettings.tsx +49 -0
- package/corpus/templates/crm/app/components/crm/settings/AttributeOptionsEditor.tsx +441 -0
- package/corpus/templates/crm/app/components/crm/settings/ConnectionSettings.tsx +169 -0
- package/corpus/templates/crm/app/components/crm/settings/FieldsSettings.tsx +935 -0
- package/corpus/templates/crm/app/components/crm/settings/ListsSettings.tsx +339 -0
- package/corpus/templates/crm/app/components/crm/settings/settings-admin.ts +377 -0
- package/corpus/templates/crm/app/components/crm/shared/AttributeValueParts.tsx +69 -0
- package/corpus/templates/crm/app/components/crm/shared/attribute-value.ts +591 -0
- package/corpus/templates/crm/app/components/layout/CrmCommandMenu.tsx +556 -0
- package/corpus/templates/crm/app/components/layout/CrmLayout.tsx +130 -0
- package/corpus/templates/crm/app/components/layout/CrmSidebar.tsx +407 -0
- package/corpus/templates/crm/app/components/layout/command-palette-search.ts +126 -0
- package/corpus/templates/crm/app/components/layout/crm-ui-intents.ts +41 -0
- package/corpus/templates/crm/app/components/layout/sidebar-collapse.ts +40 -0
- package/corpus/templates/crm/app/components/layout/sidebar-lists.ts +109 -0
- package/corpus/templates/crm/app/components/ui/alert-dialog.tsx +1 -0
- package/corpus/templates/crm/app/components/ui/badge.tsx +1 -0
- package/corpus/templates/crm/app/components/ui/button.tsx +1 -0
- package/corpus/templates/crm/app/components/ui/card.tsx +1 -0
- package/corpus/templates/crm/app/components/ui/checkbox.tsx +1 -0
- package/corpus/templates/crm/app/components/ui/dialog.tsx +1 -0
- package/corpus/templates/crm/app/components/ui/dropdown-menu.tsx +1 -0
- package/corpus/templates/crm/app/components/ui/hover-card.tsx +1 -0
- package/corpus/templates/crm/app/components/ui/input.tsx +1 -0
- package/corpus/templates/crm/app/components/ui/label.tsx +1 -0
- package/corpus/templates/crm/app/components/ui/popover.tsx +1 -0
- package/corpus/templates/crm/app/components/ui/select.tsx +1 -0
- package/corpus/templates/crm/app/components/ui/sheet.tsx +1 -0
- package/corpus/templates/crm/app/components/ui/skeleton.tsx +1 -0
- package/corpus/templates/crm/app/components/ui/switch.tsx +1 -0
- package/corpus/templates/crm/app/components/ui/table.tsx +1 -0
- package/corpus/templates/crm/app/components/ui/tabs.tsx +1 -0
- package/corpus/templates/crm/app/components/ui/textarea.tsx +1 -0
- package/corpus/templates/crm/app/components/ui/toolkit-provider.tsx +17 -0
- package/corpus/templates/crm/app/components/ui/tooltip.tsx +1 -0
- package/corpus/templates/crm/app/design-system.ts +3 -0
- package/corpus/templates/crm/app/entry.client.tsx +19 -0
- package/corpus/templates/crm/app/entry.server.tsx +10 -0
- package/corpus/templates/crm/app/global.css +167 -0
- package/corpus/templates/crm/app/hooks/use-keyboard-shortcuts.ts +206 -0
- package/corpus/templates/crm/app/hooks/use-navigation-state.ts +48 -0
- package/corpus/templates/crm/app/i18n/en-US.ts +684 -0
- package/corpus/templates/crm/app/i18n/index.ts +34 -0
- package/corpus/templates/crm/app/lib/dashboard.ts +13 -0
- package/corpus/templates/crm/app/lib/navigation.ts +17 -0
- package/corpus/templates/crm/app/lib/tab-id.ts +1 -0
- package/corpus/templates/crm/app/lib/types.ts +172 -0
- package/corpus/templates/crm/app/lib/utils.ts +1 -0
- package/corpus/templates/crm/app/root.tsx +93 -0
- package/corpus/templates/crm/app/routes/_index.tsx +26 -0
- package/corpus/templates/crm/app/routes/accounts.tsx +41 -0
- package/corpus/templates/crm/app/routes/agent.tsx +5 -0
- package/corpus/templates/crm/app/routes/ask.tsx +53 -0
- package/corpus/templates/crm/app/routes/dashboard.tsx +118 -0
- package/corpus/templates/crm/app/routes/lists.tsx +228 -0
- package/corpus/templates/crm/app/routes/opportunities.tsx +41 -0
- package/corpus/templates/crm/app/routes/people.tsx +41 -0
- package/corpus/templates/crm/app/routes/proposals.tsx +388 -0
- package/corpus/templates/crm/app/routes/records.$recordId.tsx +53 -0
- package/corpus/templates/crm/app/routes/records.tsx +64 -0
- package/corpus/templates/crm/app/routes/settings.tsx +118 -0
- package/corpus/templates/crm/app/routes/setup.tsx +308 -0
- package/corpus/templates/crm/app/routes/tasks.tsx +252 -0
- package/corpus/templates/crm/app/routes/views.tsx +683 -0
- package/corpus/templates/crm/app/routes.ts +19 -0
- package/corpus/templates/crm/app/vite-env.d.ts +6 -0
- package/corpus/templates/crm/changelog/2026-07-26-any-list-or-view-can-be-shown-as-a-board-grouped-by-stage-so.md +6 -0
- package/corpus/templates/crm/changelog/2026-07-26-crm-now-finds-likely-duplicate-records-and-tells-you-exactly.md +6 -0
- package/corpus/templates/crm/changelog/2026-07-26-enrichment-now-shows-what-a-run-will-cost-before-it-starts-g.md +6 -0
- package/corpus/templates/crm/changelog/2026-07-26-lists-turn-any-set-of-records-into-a-pipeline-with-its-own-s.md +6 -0
- package/corpus/templates/crm/changelog/2026-07-26-moving-a-record-or-list-entry-into-a-stage-that-was-retired-.md +6 -0
- package/corpus/templates/crm/changelog/2026-07-26-opening-a-record-now-shows-one-page-with-its-fields-the-list.md +6 -0
- package/corpus/templates/crm/changelog/2026-07-26-press-cmd-k-to-jump-to-any-record-list-view-or-setting-and-u.md +6 -0
- package/corpus/templates/crm/changelog/2026-07-26-provider-record-edits-now-show-an-exact-before-after-diff-an.md +6 -0
- package/corpus/templates/crm/changelog/2026-07-26-record-page-attribute-labels-now-read-in-full-instead-of-bei.md +6 -0
- package/corpus/templates/crm/changelog/2026-07-26-records-now-use-typed-fields-currency-dates-ratings-status-s.md +6 -0
- package/corpus/templates/crm/changelog/2026-07-26-retyping-a-value-on-the-record-page-now-replaces-it-instead-.md +6 -0
- package/corpus/templates/crm/changelog/2026-07-26-settings-now-has-fields-and-lists-tabs-for-adding-fields-edi.md +6 -0
- package/corpus/templates/crm/changelog/2026-07-26-the-proposal-review-now-shows-the-field-that-actually-change.md +6 -0
- package/corpus/templates/crm/changelog/2026-07-26-the-record-grid-is-now-a-real-spreadsheet-filter-sort-and-pa.md +6 -0
- package/corpus/templates/crm/components.json +20 -0
- package/corpus/templates/crm/docs/architecture/crm-contract.md +214 -0
- package/corpus/templates/crm/docs/features/README.md +55 -0
- package/corpus/templates/crm/docs/features/f1-records-and-fields.mdx +52 -0
- package/corpus/templates/crm/docs/features/f2-lists-pipelines-and-views.mdx +48 -0
- package/corpus/templates/crm/docs/features/f3-work-and-record-quality.mdx +40 -0
- package/corpus/templates/crm/docs/features/f4-evidence-and-chat.mdx +47 -0
- package/corpus/templates/crm/learnings.defaults.md +5 -0
- package/corpus/templates/crm/netlify.toml +11 -0
- package/corpus/templates/crm/package.json +61 -0
- package/corpus/templates/crm/public/favicon.svg +1 -0
- package/corpus/templates/crm/public/icon-180.svg +1 -0
- package/corpus/templates/crm/public/manifest.json +11 -0
- package/corpus/templates/crm/react-router.config.ts +7 -0
- package/corpus/templates/crm/server/crm/adapter.ts +32 -0
- package/corpus/templates/crm/server/crm/crm-field-firewall.ts +64 -0
- package/corpus/templates/crm/server/crm/crm-mirror.ts +721 -0
- package/corpus/templates/crm/server/crm/field-policy-attributes.ts +48 -0
- package/corpus/templates/crm/server/crm/hubspot-adapter.ts +1146 -0
- package/corpus/templates/crm/server/crm/native-adapter.ts +1810 -0
- package/corpus/templates/crm/server/crm/provider-record-link.ts +264 -0
- package/corpus/templates/crm/server/crm/read-through.ts +207 -0
- package/corpus/templates/crm/server/crm/salesforce-adapter.ts +1255 -0
- package/corpus/templates/crm/server/db/crm-store.ts +1112 -0
- package/corpus/templates/crm/server/db/index.ts +191 -0
- package/corpus/templates/crm/server/db/schema.ts +647 -0
- package/corpus/templates/crm/server/lib/crm-query.ts +1558 -0
- package/corpus/templates/crm/server/lib/dedupe.ts +714 -0
- package/corpus/templates/crm/server/lib/enrichment-cost.ts +381 -0
- package/corpus/templates/crm/server/lib/enrichment-slots.ts +519 -0
- package/corpus/templates/crm/server/lib/intelligence/default-detectors.ts +65 -0
- package/corpus/templates/crm/server/lib/intelligence/evidence.ts +105 -0
- package/corpus/templates/crm/server/lib/intelligence/keyword-detector.ts +94 -0
- package/corpus/templates/crm/server/lib/intelligence/smart-detector.ts +39 -0
- package/corpus/templates/crm/server/lib/intelligence/summary.ts +19 -0
- package/corpus/templates/crm/server/lib/lifecycle.ts +634 -0
- package/corpus/templates/crm/server/lib/provider-api.ts +64 -0
- package/corpus/templates/crm/server/middleware/auth.ts +4 -0
- package/corpus/templates/crm/server/plugins/agent-chat.ts +65 -0
- package/corpus/templates/crm/server/plugins/auth.ts +14 -0
- package/corpus/templates/crm/server/plugins/core-routes.ts +17 -0
- package/corpus/templates/crm/server/plugins/db.ts +738 -0
- package/corpus/templates/crm/server/routes/[...page].get.ts +5 -0
- package/corpus/templates/crm/shared/crm-attributes.ts +469 -0
- package/corpus/templates/crm/shared/crm-automation-recipes.ts +132 -0
- package/corpus/templates/crm/shared/crm-contract.ts +417 -0
- package/corpus/templates/crm/shared/crm-navigation.ts +190 -0
- package/corpus/templates/crm/shared/crm-sales-config.ts +63 -0
- package/corpus/templates/crm/ssr-entry.ts +11 -0
- package/corpus/templates/crm/tsconfig.json +24 -0
- package/corpus/templates/crm/vite.config.ts +15 -0
- package/corpus/templates/crm/vitest.config.ts +5 -0
- package/corpus/templates/design/app/i18n-data.ts +12 -12
- package/corpus/templates/design/changelog/2026-07-26-apply-design-updates-from-a-centered-top-toolbar-without-cov.md +6 -0
- package/corpus/toolkit/CHANGELOG.md +6 -0
- package/corpus/toolkit/package.json +1 -1
- package/corpus/toolkit/src/editor/useCollabReconcile.ts +14 -1
- package/dist/cli/templates-meta.d.ts.map +1 -1
- package/dist/cli/templates-meta.js +12 -0
- package/dist/cli/templates-meta.js.map +1 -1
- package/dist/collab/routes.d.ts +2 -2
- package/dist/deploy/build.js +6 -0
- package/dist/deploy/build.js.map +1 -1
- package/dist/observability/routes.d.ts +3 -3
- package/dist/server/builder-browser.d.ts +13 -0
- package/dist/server/builder-browser.d.ts.map +1 -1
- package/dist/server/builder-browser.js +59 -0
- package/dist/server/builder-browser.js.map +1 -1
- package/dist/server/core-routes-plugin.d.ts.map +1 -1
- package/dist/server/core-routes-plugin.js +10 -5
- package/dist/server/core-routes-plugin.js.map +1 -1
- package/dist/server/transcribe-voice.d.ts +1 -1
- package/package.json +2 -2
- package/src/cli/templates-meta.ts +12 -0
- package/src/deploy/build.ts +8 -0
- package/src/server/builder-browser.ts +76 -0
- package/src/server/core-routes-plugin.ts +15 -5
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import type { ActionRunContext } from "@agent-native/core/action";
|
|
2
|
+
import { accessFilter, assertAccess } from "@agent-native/core/sharing";
|
|
3
|
+
import { and, eq, inArray } from "drizzle-orm";
|
|
4
|
+
|
|
5
|
+
import { getDb, schema } from "../server/db/index.js";
|
|
6
|
+
import type { CallEvidenceExcerpt } from "../server/lib/intelligence/evidence.js";
|
|
7
|
+
import {
|
|
8
|
+
requireCrmScope,
|
|
9
|
+
scopedCrmIdempotencyKey,
|
|
10
|
+
} from "./_crm-action-utils.js";
|
|
11
|
+
|
|
12
|
+
export async function loadCrmSignalEvidence(args: {
|
|
13
|
+
recordId: string;
|
|
14
|
+
evidenceIds?: string[];
|
|
15
|
+
role?: "viewer" | "editor";
|
|
16
|
+
}) {
|
|
17
|
+
await assertAccess("crm-record", args.recordId, args.role ?? "viewer");
|
|
18
|
+
const db = getDb();
|
|
19
|
+
const predicates = [
|
|
20
|
+
eq(schema.crmCallEvidence.recordId, args.recordId),
|
|
21
|
+
accessFilter(schema.crmCallEvidence, schema.crmCallEvidenceShares),
|
|
22
|
+
];
|
|
23
|
+
if (args.evidenceIds?.length) {
|
|
24
|
+
predicates.push(inArray(schema.crmCallEvidence.id, args.evidenceIds));
|
|
25
|
+
}
|
|
26
|
+
const rows = await db
|
|
27
|
+
.select()
|
|
28
|
+
.from(schema.crmCallEvidence)
|
|
29
|
+
.where(and(...predicates))
|
|
30
|
+
.limit(20);
|
|
31
|
+
if (
|
|
32
|
+
args.evidenceIds?.length &&
|
|
33
|
+
rows.length !== new Set(args.evidenceIds).size
|
|
34
|
+
) {
|
|
35
|
+
throw new Error("One or more call-evidence references are unavailable.");
|
|
36
|
+
}
|
|
37
|
+
return rows;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export function evidenceExcerpts(
|
|
41
|
+
rows: Awaited<ReturnType<typeof loadCrmSignalEvidence>>,
|
|
42
|
+
): CallEvidenceExcerpt[] {
|
|
43
|
+
return rows.flatMap((row) => {
|
|
44
|
+
const quote = row.quote.trim() || row.summary.trim();
|
|
45
|
+
if (!quote) return [];
|
|
46
|
+
return [
|
|
47
|
+
{
|
|
48
|
+
evidenceRef: row.id,
|
|
49
|
+
quote,
|
|
50
|
+
...(row.speaker ? { speaker: row.speaker } : {}),
|
|
51
|
+
startSeconds: row.startSeconds ?? 0,
|
|
52
|
+
...(row.endSeconds !== null ? { endSeconds: row.endSeconds } : {}),
|
|
53
|
+
},
|
|
54
|
+
];
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export async function crmSignalIdempotencyKey(
|
|
59
|
+
ctx: ActionRunContext | undefined,
|
|
60
|
+
recordId: string,
|
|
61
|
+
key: string,
|
|
62
|
+
) {
|
|
63
|
+
const scope = requireCrmScope(ctx);
|
|
64
|
+
return {
|
|
65
|
+
scope,
|
|
66
|
+
key: await scopedCrmIdempotencyKey({
|
|
67
|
+
ownerEmail: scope.ownerEmail,
|
|
68
|
+
orgId: scope.orgId,
|
|
69
|
+
recordId,
|
|
70
|
+
key: `signal:${key}`,
|
|
71
|
+
}),
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function signalTupleError(message: string): never {
|
|
76
|
+
const error = new Error(message) as Error & { statusCode?: number };
|
|
77
|
+
error.statusCode = 404;
|
|
78
|
+
throw error;
|
|
79
|
+
}
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
import { defineAction, type ActionRunContext } from "@agent-native/core/action";
|
|
2
|
+
import { accessFilter } from "@agent-native/core/sharing";
|
|
3
|
+
import { and, desc, eq } from "drizzle-orm";
|
|
4
|
+
import { z } from "zod";
|
|
5
|
+
|
|
6
|
+
import { getDb, schema } from "../server/db/index.js";
|
|
7
|
+
import {
|
|
8
|
+
isSafeCrmMutationFields,
|
|
9
|
+
MAX_CRM_FIELDS_PER_MUTATION,
|
|
10
|
+
requireCrmScope,
|
|
11
|
+
} from "./_crm-action-utils.js";
|
|
12
|
+
import {
|
|
13
|
+
CrmListError,
|
|
14
|
+
crmActorFrom,
|
|
15
|
+
indexAttributes,
|
|
16
|
+
loadCrmListAttributes,
|
|
17
|
+
requireCrmList,
|
|
18
|
+
writeCrmListEntryValues,
|
|
19
|
+
} from "./_crm-list-utils.js";
|
|
20
|
+
|
|
21
|
+
export const listEntryValuesSchema = z
|
|
22
|
+
.record(z.string().trim().min(1).max(120), z.unknown())
|
|
23
|
+
.refine(
|
|
24
|
+
(values) => Object.keys(values).length <= MAX_CRM_FIELDS_PER_MUTATION,
|
|
25
|
+
`Provide at most ${MAX_CRM_FIELDS_PER_MUTATION} entry attributes.`,
|
|
26
|
+
)
|
|
27
|
+
.refine(
|
|
28
|
+
isSafeCrmMutationFields,
|
|
29
|
+
"CRM entry values cannot contain media, transcripts, data URLs, base64, or oversized JSON.",
|
|
30
|
+
);
|
|
31
|
+
|
|
32
|
+
export default defineAction({
|
|
33
|
+
description:
|
|
34
|
+
"Add a record to a CRM list as a new entry, with optional starting values for the list's own entry attributes. A record may hold more than one entry in the same list — two open deals for one company are two entries — so this never de-duplicates; it returns existingEntryIds for the caller to decide. The record's objectType must match the list's parentObjectType. The entry is local-authoritative even when the record mirrors HubSpot or Salesforce.",
|
|
35
|
+
schema: z.object({
|
|
36
|
+
listId: z.string().trim().min(1).max(128),
|
|
37
|
+
recordId: z.string().trim().min(1).max(128),
|
|
38
|
+
values: listEntryValuesSchema.optional(),
|
|
39
|
+
position: z.number().int().min(0).max(100_000).optional(),
|
|
40
|
+
}),
|
|
41
|
+
audit: {
|
|
42
|
+
target: (_args, result) => {
|
|
43
|
+
const entry = result as {
|
|
44
|
+
entryId: string;
|
|
45
|
+
ownerEmail: string;
|
|
46
|
+
orgId: string | null;
|
|
47
|
+
visibility: "private" | "org";
|
|
48
|
+
};
|
|
49
|
+
return {
|
|
50
|
+
type: "crm-list-entry",
|
|
51
|
+
id: entry.entryId,
|
|
52
|
+
ownerEmail: entry.ownerEmail,
|
|
53
|
+
orgId: entry.orgId,
|
|
54
|
+
visibility: entry.visibility,
|
|
55
|
+
};
|
|
56
|
+
},
|
|
57
|
+
summary: (args) =>
|
|
58
|
+
`Added CRM record ${args.recordId} to list ${args.listId}`,
|
|
59
|
+
recordInputs: false,
|
|
60
|
+
},
|
|
61
|
+
run: async (args, ctx?: ActionRunContext) => {
|
|
62
|
+
const ownership = requireCrmScope(ctx);
|
|
63
|
+
const db = getDb();
|
|
64
|
+
const list = await requireCrmList(db, args.listId, "editor");
|
|
65
|
+
|
|
66
|
+
const [record] = await db
|
|
67
|
+
.select({
|
|
68
|
+
id: schema.crmRecords.id,
|
|
69
|
+
objectType: schema.crmRecords.objectType,
|
|
70
|
+
displayName: schema.crmRecords.displayName,
|
|
71
|
+
})
|
|
72
|
+
.from(schema.crmRecords)
|
|
73
|
+
.where(
|
|
74
|
+
and(
|
|
75
|
+
eq(schema.crmRecords.id, args.recordId),
|
|
76
|
+
eq(schema.crmRecords.tombstone, false),
|
|
77
|
+
accessFilter(schema.crmRecords, schema.crmRecordShares),
|
|
78
|
+
),
|
|
79
|
+
)
|
|
80
|
+
.limit(1);
|
|
81
|
+
if (!record) {
|
|
82
|
+
throw new CrmListError(
|
|
83
|
+
"crm-record-not-found",
|
|
84
|
+
`CRM record "${args.recordId}" was not found or is not visible to you.`,
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
if (record.objectType !== list.parentObjectType) {
|
|
88
|
+
throw new CrmListError(
|
|
89
|
+
"crm-list-object-type-mismatch",
|
|
90
|
+
`List "${list.name}" holds ${list.parentObjectType}; record ${record.id} is ${record.objectType}.`,
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const existing = await db
|
|
95
|
+
.select({ id: schema.crmListEntries.id })
|
|
96
|
+
.from(schema.crmListEntries)
|
|
97
|
+
.where(
|
|
98
|
+
and(
|
|
99
|
+
eq(schema.crmListEntries.listId, list.id),
|
|
100
|
+
eq(schema.crmListEntries.recordId, record.id),
|
|
101
|
+
accessFilter(schema.crmListEntries, schema.crmListEntryShares),
|
|
102
|
+
),
|
|
103
|
+
)
|
|
104
|
+
.limit(50);
|
|
105
|
+
|
|
106
|
+
const attributes = indexAttributes(
|
|
107
|
+
await loadCrmListAttributes(db, list.id),
|
|
108
|
+
);
|
|
109
|
+
const [last] = await db
|
|
110
|
+
.select({ position: schema.crmListEntries.position })
|
|
111
|
+
.from(schema.crmListEntries)
|
|
112
|
+
.where(
|
|
113
|
+
and(
|
|
114
|
+
eq(schema.crmListEntries.listId, list.id),
|
|
115
|
+
accessFilter(schema.crmListEntries, schema.crmListEntryShares),
|
|
116
|
+
),
|
|
117
|
+
)
|
|
118
|
+
.orderBy(desc(schema.crmListEntries.position))
|
|
119
|
+
.limit(1);
|
|
120
|
+
|
|
121
|
+
const entryId = crypto.randomUUID();
|
|
122
|
+
const actor = crmActorFrom(ctx);
|
|
123
|
+
const now = new Date().toISOString();
|
|
124
|
+
|
|
125
|
+
// One transaction so a rejected value (an unknown status option, say) can
|
|
126
|
+
// never leave a half-populated entry behind.
|
|
127
|
+
const writes = await db.transaction(async (tx) => {
|
|
128
|
+
await tx.insert(schema.crmListEntries).values({
|
|
129
|
+
id: entryId,
|
|
130
|
+
listId: list.id,
|
|
131
|
+
recordId: record.id,
|
|
132
|
+
position: args.position ?? (last ? last.position + 1 : 0),
|
|
133
|
+
createdByActorType: actor.type,
|
|
134
|
+
createdByActorId: actor.id,
|
|
135
|
+
...ownership,
|
|
136
|
+
createdAt: now,
|
|
137
|
+
updatedAt: now,
|
|
138
|
+
});
|
|
139
|
+
return writeCrmListEntryValues({
|
|
140
|
+
db: tx,
|
|
141
|
+
recordId: record.id,
|
|
142
|
+
entryId,
|
|
143
|
+
attributes,
|
|
144
|
+
values: args.values ?? {},
|
|
145
|
+
actor,
|
|
146
|
+
ownership,
|
|
147
|
+
now,
|
|
148
|
+
});
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
return {
|
|
152
|
+
entryId,
|
|
153
|
+
listId: list.id,
|
|
154
|
+
recordId: record.id,
|
|
155
|
+
/**
|
|
156
|
+
* Entries this record already had in this list. Non-empty is normal, not
|
|
157
|
+
* an error — the caller decides whether a second entry was intended.
|
|
158
|
+
*/
|
|
159
|
+
existingEntryIds: existing.map((entry) => entry.id),
|
|
160
|
+
values: writes,
|
|
161
|
+
...ownership,
|
|
162
|
+
};
|
|
163
|
+
},
|
|
164
|
+
});
|
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
import { defineAction, type ActionRunContext } from "@agent-native/core/action";
|
|
2
|
+
import { accessFilter, assertAccess } from "@agent-native/core/sharing";
|
|
3
|
+
import { and, eq, inArray, isNull } from "drizzle-orm";
|
|
4
|
+
import { z } from "zod";
|
|
5
|
+
|
|
6
|
+
import { isConnectedCrmProvider } from "../server/crm/adapter.js";
|
|
7
|
+
import {
|
|
8
|
+
resolveProviderRecordLink,
|
|
9
|
+
type ProviderRecordLink,
|
|
10
|
+
} from "../server/crm/provider-record-link.js";
|
|
11
|
+
import { getDb, schema } from "../server/db/index.js";
|
|
12
|
+
import {
|
|
13
|
+
isSafeCrmMutationFields,
|
|
14
|
+
parseJsonRecord,
|
|
15
|
+
requireCrmScope,
|
|
16
|
+
} from "./_crm-action-utils.js";
|
|
17
|
+
|
|
18
|
+
const PROVIDER_LABELS = {
|
|
19
|
+
hubspot: "HubSpot",
|
|
20
|
+
salesforce: "Salesforce",
|
|
21
|
+
} as const;
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* A prepared field change. `beforeKnown` separates "the mirror holds no value
|
|
25
|
+
* for this field" from "the mirrored value is empty" — collapsing them would
|
|
26
|
+
* show a confident `— → Renewal` diff for a field we simply do not mirror.
|
|
27
|
+
*/
|
|
28
|
+
interface PreparedField {
|
|
29
|
+
name: string;
|
|
30
|
+
beforeKnown: boolean;
|
|
31
|
+
before: string | number | boolean | null;
|
|
32
|
+
after: unknown;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function updateAffectedRows(result: unknown): number {
|
|
36
|
+
if (!result || typeof result !== "object") return 0;
|
|
37
|
+
const value = result as {
|
|
38
|
+
rowsAffected?: unknown;
|
|
39
|
+
rowCount?: unknown;
|
|
40
|
+
count?: unknown;
|
|
41
|
+
changes?: unknown;
|
|
42
|
+
meta?: { changes?: unknown };
|
|
43
|
+
};
|
|
44
|
+
for (const count of [
|
|
45
|
+
value.rowsAffected,
|
|
46
|
+
value.rowCount,
|
|
47
|
+
value.count,
|
|
48
|
+
value.changes,
|
|
49
|
+
value.meta?.changes,
|
|
50
|
+
]) {
|
|
51
|
+
if (typeof count === "number") return count;
|
|
52
|
+
}
|
|
53
|
+
return 0;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function currentMirroredValue(row: {
|
|
57
|
+
stringValue: string | null;
|
|
58
|
+
numberValue: number | null;
|
|
59
|
+
booleanValue: boolean | null;
|
|
60
|
+
jsonValue: string | null;
|
|
61
|
+
}): string | number | boolean | null {
|
|
62
|
+
if (row.stringValue !== null) return row.stringValue;
|
|
63
|
+
if (row.numberValue !== null) return row.numberValue;
|
|
64
|
+
if (row.booleanValue !== null) return row.booleanValue;
|
|
65
|
+
return row.jsonValue;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export default defineAction({
|
|
69
|
+
description:
|
|
70
|
+
"Prepare one pending HubSpot or Salesforce record change for completion upstream. Returns the exact before/after diff and a deep link to the record, and records the handoff in the proposal ledger. It never writes to the provider and never reports an upstream change as applied.",
|
|
71
|
+
schema: z.object({
|
|
72
|
+
proposalId: z.string().trim().min(1).max(128),
|
|
73
|
+
}),
|
|
74
|
+
needsApproval: true,
|
|
75
|
+
audit: {
|
|
76
|
+
target: (_args, result) => {
|
|
77
|
+
const response = result as {
|
|
78
|
+
recordId: string;
|
|
79
|
+
ownerEmail: string;
|
|
80
|
+
orgId: string | null;
|
|
81
|
+
visibility: "private" | "org";
|
|
82
|
+
};
|
|
83
|
+
return {
|
|
84
|
+
type: "crm-record",
|
|
85
|
+
id: response.recordId,
|
|
86
|
+
ownerEmail: response.ownerEmail,
|
|
87
|
+
orgId: response.orgId,
|
|
88
|
+
visibility: response.visibility,
|
|
89
|
+
};
|
|
90
|
+
},
|
|
91
|
+
summary: (args) =>
|
|
92
|
+
`Prepared CRM proposal ${args.proposalId} for upstream completion`,
|
|
93
|
+
recordInputs: false,
|
|
94
|
+
},
|
|
95
|
+
run: async (args, ctx?: ActionRunContext) => {
|
|
96
|
+
await assertAccess("crm-mutation", args.proposalId, "editor");
|
|
97
|
+
const db = getDb();
|
|
98
|
+
const [proposal] = await db
|
|
99
|
+
.select()
|
|
100
|
+
.from(schema.crmMutations)
|
|
101
|
+
.where(
|
|
102
|
+
and(
|
|
103
|
+
eq(schema.crmMutations.id, args.proposalId),
|
|
104
|
+
accessFilter(
|
|
105
|
+
schema.crmMutations,
|
|
106
|
+
schema.crmMutationShares,
|
|
107
|
+
undefined,
|
|
108
|
+
"editor",
|
|
109
|
+
),
|
|
110
|
+
),
|
|
111
|
+
)
|
|
112
|
+
.limit(1);
|
|
113
|
+
if (!proposal) throw new Error("CRM proposal was not found.");
|
|
114
|
+
if (proposal.target !== "provider" || proposal.operation !== "update") {
|
|
115
|
+
throw new Error(
|
|
116
|
+
"Only pending provider update proposals can be prepared in this phase.",
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
if (proposal.status !== "pending") {
|
|
120
|
+
throw new Error(
|
|
121
|
+
`CRM proposal is ${proposal.status} and cannot be prepared again.`,
|
|
122
|
+
);
|
|
123
|
+
}
|
|
124
|
+
if (!proposal.recordId || !proposal.connectionId) {
|
|
125
|
+
throw new Error(
|
|
126
|
+
"CRM proposal is missing its record or connection reference.",
|
|
127
|
+
);
|
|
128
|
+
}
|
|
129
|
+
await assertAccess("crm-record", proposal.recordId, "editor");
|
|
130
|
+
await assertAccess("crm-connection", proposal.connectionId, "editor");
|
|
131
|
+
const [[record], [connection]] = await Promise.all([
|
|
132
|
+
db
|
|
133
|
+
.select()
|
|
134
|
+
.from(schema.crmRecords)
|
|
135
|
+
.where(
|
|
136
|
+
and(
|
|
137
|
+
eq(schema.crmRecords.id, proposal.recordId),
|
|
138
|
+
accessFilter(
|
|
139
|
+
schema.crmRecords,
|
|
140
|
+
schema.crmRecordShares,
|
|
141
|
+
undefined,
|
|
142
|
+
"editor",
|
|
143
|
+
),
|
|
144
|
+
),
|
|
145
|
+
)
|
|
146
|
+
.limit(1),
|
|
147
|
+
db
|
|
148
|
+
.select()
|
|
149
|
+
.from(schema.crmConnections)
|
|
150
|
+
.where(
|
|
151
|
+
and(
|
|
152
|
+
eq(schema.crmConnections.id, proposal.connectionId),
|
|
153
|
+
accessFilter(
|
|
154
|
+
schema.crmConnections,
|
|
155
|
+
schema.crmConnectionShares,
|
|
156
|
+
undefined,
|
|
157
|
+
"editor",
|
|
158
|
+
),
|
|
159
|
+
),
|
|
160
|
+
)
|
|
161
|
+
.limit(1),
|
|
162
|
+
]);
|
|
163
|
+
if (!record || record.tombstone || !connection) {
|
|
164
|
+
throw new Error(
|
|
165
|
+
"CRM proposal no longer has an available record and connection.",
|
|
166
|
+
);
|
|
167
|
+
}
|
|
168
|
+
if (!isConnectedCrmProvider(connection.provider)) {
|
|
169
|
+
throw new Error(
|
|
170
|
+
"Only HubSpot and Salesforce provider proposals are enabled in this release.",
|
|
171
|
+
);
|
|
172
|
+
}
|
|
173
|
+
if (!connection.workspaceConnectionId) {
|
|
174
|
+
throw new Error(
|
|
175
|
+
"CRM connection is missing its workspace connection reference.",
|
|
176
|
+
);
|
|
177
|
+
}
|
|
178
|
+
// Without a revision anchor the mirrored "before" column cannot be trusted
|
|
179
|
+
// to describe the record the user is about to edit by hand.
|
|
180
|
+
if (!proposal.expectedRemoteRevision) {
|
|
181
|
+
throw new Error(
|
|
182
|
+
"CRM proposal has no remote revision and must be recreated from a refreshed record.",
|
|
183
|
+
);
|
|
184
|
+
}
|
|
185
|
+
const patch = parseJsonRecord(proposal.patchJson);
|
|
186
|
+
const fields = patch.fields;
|
|
187
|
+
if (!fields || typeof fields !== "object" || Array.isArray(fields)) {
|
|
188
|
+
throw new Error("CRM proposal has an invalid field patch.");
|
|
189
|
+
}
|
|
190
|
+
const fieldPatch = fields as Record<string, unknown>;
|
|
191
|
+
if (!isSafeCrmMutationFields(fieldPatch)) {
|
|
192
|
+
throw new Error("CRM proposal contains an unsafe field patch.");
|
|
193
|
+
}
|
|
194
|
+
const fieldNames = Object.keys(fieldPatch);
|
|
195
|
+
|
|
196
|
+
const currentRows = fieldNames.length
|
|
197
|
+
? await db
|
|
198
|
+
.select({
|
|
199
|
+
fieldName: schema.crmRecordFields.fieldName,
|
|
200
|
+
stringValue: schema.crmRecordFields.stringValue,
|
|
201
|
+
numberValue: schema.crmRecordFields.numberValue,
|
|
202
|
+
booleanValue: schema.crmRecordFields.booleanValue,
|
|
203
|
+
jsonValue: schema.crmRecordFields.jsonValue,
|
|
204
|
+
})
|
|
205
|
+
.from(schema.crmRecordFields)
|
|
206
|
+
.where(
|
|
207
|
+
and(
|
|
208
|
+
eq(schema.crmRecordFields.recordId, record.id),
|
|
209
|
+
isNull(schema.crmRecordFields.entryId),
|
|
210
|
+
isNull(schema.crmRecordFields.activeUntil),
|
|
211
|
+
inArray(schema.crmRecordFields.fieldName, fieldNames),
|
|
212
|
+
accessFilter(schema.crmRecordFields, schema.crmRecordFieldShares),
|
|
213
|
+
),
|
|
214
|
+
)
|
|
215
|
+
: [];
|
|
216
|
+
const currentByFieldName = new Map(
|
|
217
|
+
currentRows.map((row) => [row.fieldName, row]),
|
|
218
|
+
);
|
|
219
|
+
const preparedFields: PreparedField[] = fieldNames.map((name) => {
|
|
220
|
+
const current = currentByFieldName.get(name);
|
|
221
|
+
return {
|
|
222
|
+
name,
|
|
223
|
+
beforeKnown: Boolean(current),
|
|
224
|
+
before: current ? currentMirroredValue(current) : null,
|
|
225
|
+
after: fieldPatch[name],
|
|
226
|
+
};
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
const link: ProviderRecordLink = await resolveProviderRecordLink({
|
|
230
|
+
provider: connection.provider,
|
|
231
|
+
objectType: record.objectType,
|
|
232
|
+
remoteId: record.remoteId,
|
|
233
|
+
accountId: connection.accountId,
|
|
234
|
+
workspaceConnectionId: connection.workspaceConnectionId,
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
const scope = requireCrmScope(ctx);
|
|
238
|
+
const now = new Date().toISOString();
|
|
239
|
+
// `approved` with `appliedAt` still null is the ledger's "prepared and
|
|
240
|
+
// handed off, never written upstream" state. `error` stays null so a
|
|
241
|
+
// working handoff is never mistaken for a failure.
|
|
242
|
+
const claim = await db
|
|
243
|
+
.update(schema.crmMutations)
|
|
244
|
+
.set({
|
|
245
|
+
status: "approved",
|
|
246
|
+
approvedBy: scope.ownerEmail,
|
|
247
|
+
approvedAt: now,
|
|
248
|
+
error: null,
|
|
249
|
+
updatedAt: now,
|
|
250
|
+
})
|
|
251
|
+
.where(
|
|
252
|
+
and(
|
|
253
|
+
eq(schema.crmMutations.id, proposal.id),
|
|
254
|
+
eq(schema.crmMutations.status, "pending"),
|
|
255
|
+
accessFilter(
|
|
256
|
+
schema.crmMutations,
|
|
257
|
+
schema.crmMutationShares,
|
|
258
|
+
undefined,
|
|
259
|
+
"editor",
|
|
260
|
+
),
|
|
261
|
+
),
|
|
262
|
+
);
|
|
263
|
+
if (updateAffectedRows(claim) !== 1) {
|
|
264
|
+
throw new Error(
|
|
265
|
+
"CRM proposal was already claimed by another preparation attempt.",
|
|
266
|
+
);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
const providerLabel = PROVIDER_LABELS[connection.provider];
|
|
270
|
+
return {
|
|
271
|
+
proposalId: proposal.id,
|
|
272
|
+
recordId: record.id,
|
|
273
|
+
recordName: record.displayName,
|
|
274
|
+
status: "approved" as const,
|
|
275
|
+
/** This release never completes a provider write. Always false. */
|
|
276
|
+
upstreamApplied: false,
|
|
277
|
+
provider: connection.provider,
|
|
278
|
+
providerLabel,
|
|
279
|
+
preparedAt: now,
|
|
280
|
+
fields: preparedFields,
|
|
281
|
+
recordUrl: link.available ? link.url : null,
|
|
282
|
+
recordUrlUnavailableReason: link.available ? null : link.reason,
|
|
283
|
+
guidance: link.available
|
|
284
|
+
? `Reviewed and ready. Open the record in ${providerLabel} and make this change there — CRM has not written it upstream.`
|
|
285
|
+
: `Reviewed and ready. CRM could not build a ${providerLabel} record link (${link.reason}); find the record in ${providerLabel} and make this change there. CRM has not written it upstream.`,
|
|
286
|
+
ownerEmail: record.ownerEmail,
|
|
287
|
+
orgId: record.orgId,
|
|
288
|
+
visibility: record.visibility,
|
|
289
|
+
};
|
|
290
|
+
},
|
|
291
|
+
});
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { defineAction } from "@agent-native/core/action";
|
|
2
|
+
import { accessFilter } from "@agent-native/core/sharing";
|
|
3
|
+
import { and, count, eq, isNull } from "drizzle-orm";
|
|
4
|
+
import { z } from "zod";
|
|
5
|
+
|
|
6
|
+
import { getDb, schema } from "../server/db/index.js";
|
|
7
|
+
import {
|
|
8
|
+
loadAttributeOptions,
|
|
9
|
+
requireEditableAttribute,
|
|
10
|
+
toAttributeDefinition,
|
|
11
|
+
} from "./_crm-attribute-utils.js";
|
|
12
|
+
|
|
13
|
+
export default defineAction({
|
|
14
|
+
description:
|
|
15
|
+
"Archive one CRM attribute. This is a soft archive: the attribute stops appearing in the grid and pickers, and every stored value and history row it owns is retained untouched. Attributes are never deleted. Use update-crm-attribute with archived=false to restore one.",
|
|
16
|
+
schema: z.object({
|
|
17
|
+
attributeId: z.string().trim().min(1).max(128),
|
|
18
|
+
}),
|
|
19
|
+
audit: {
|
|
20
|
+
target: (args) => ({ type: "crm-field-policy", id: args.attributeId }),
|
|
21
|
+
summary: (args) => `Archived CRM attribute ${args.attributeId}`,
|
|
22
|
+
},
|
|
23
|
+
run: async (args) => {
|
|
24
|
+
const current = await requireEditableAttribute(args.attributeId);
|
|
25
|
+
const db = getDb();
|
|
26
|
+
|
|
27
|
+
if (!current.archived) {
|
|
28
|
+
await db
|
|
29
|
+
.update(schema.crmFieldPolicies)
|
|
30
|
+
.set({ archived: true, updatedAt: new Date().toISOString() })
|
|
31
|
+
.where(eq(schema.crmFieldPolicies.id, args.attributeId));
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// Reported so a caller can see the archive kept data rather than removed it.
|
|
35
|
+
const [values] = await db
|
|
36
|
+
.select({ total: count() })
|
|
37
|
+
.from(schema.crmRecordFields)
|
|
38
|
+
.where(
|
|
39
|
+
and(
|
|
40
|
+
eq(schema.crmRecordFields.attributeId, args.attributeId),
|
|
41
|
+
isNull(schema.crmRecordFields.activeUntil),
|
|
42
|
+
accessFilter(schema.crmRecordFields, schema.crmRecordFieldShares),
|
|
43
|
+
),
|
|
44
|
+
);
|
|
45
|
+
|
|
46
|
+
if (!values) {
|
|
47
|
+
throw new Error(
|
|
48
|
+
"Retained CRM attribute values could not be counted; the archive was not confirmed.",
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const [row] = await db
|
|
53
|
+
.select()
|
|
54
|
+
.from(schema.crmFieldPolicies)
|
|
55
|
+
.where(eq(schema.crmFieldPolicies.id, args.attributeId))
|
|
56
|
+
.limit(1);
|
|
57
|
+
if (!row) {
|
|
58
|
+
throw new Error("CRM attribute could not be verified after archiving.");
|
|
59
|
+
}
|
|
60
|
+
const options = await loadAttributeOptions({
|
|
61
|
+
attributeIds: [row.id],
|
|
62
|
+
includeArchived: true,
|
|
63
|
+
});
|
|
64
|
+
return {
|
|
65
|
+
...toAttributeDefinition(row, options.get(row.id) ?? []),
|
|
66
|
+
retainedValueCount: values.total,
|
|
67
|
+
};
|
|
68
|
+
},
|
|
69
|
+
});
|