@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.
Files changed (334) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/core/CHANGELOG.md +24 -0
  3. package/corpus/core/package.json +1 -1
  4. package/corpus/core/src/cli/templates-meta.ts +12 -0
  5. package/corpus/core/src/deploy/build.ts +8 -0
  6. package/corpus/core/src/server/builder-browser.ts +76 -0
  7. package/corpus/core/src/server/core-routes-plugin.ts +15 -5
  8. package/corpus/templates/assets/app/global.css +0 -4
  9. package/corpus/templates/assets/app/routes/brand-kits.$id.tsx +195 -167
  10. package/corpus/templates/assets/changelog/2026-07-23-drafts-preview-panel-now-fills-the-full-width-with-a-large-p.md +6 -0
  11. package/corpus/templates/content/actions/_builder-cms-source-adapter.ts +1 -0
  12. package/corpus/templates/content/actions/_builder-cms-write-client.ts +55 -9
  13. package/corpus/templates/content/actions/_database-source-utils.ts +756 -182
  14. package/corpus/templates/content/actions/change-content-database-source-role.ts +21 -20
  15. package/corpus/templates/content/actions/execute-builder-source-execution.ts +49 -8
  16. package/corpus/templates/content/actions/materialize-builder-required-fields.ts +42 -9
  17. package/corpus/templates/content/actions/prepare-builder-source-review.ts +88 -90
  18. package/corpus/templates/content/actions/preview-builder-source-review.ts +4 -3
  19. package/corpus/templates/content/actions/refresh-content-database-source.ts +36 -9
  20. package/corpus/templates/content/actions/set-content-database-source-write-mode.ts +28 -24
  21. package/corpus/templates/content/app/components/editor/BubbleToolbar.tsx +62 -12
  22. package/corpus/templates/content/app/components/editor/DocumentEditor.tsx +91 -16
  23. package/corpus/templates/content/app/components/editor/DocumentProperties.tsx +15 -1
  24. package/corpus/templates/content/app/components/editor/SlashCommandMenu.tsx +234 -44
  25. package/corpus/templates/content/app/components/editor/VisualEditor.tsx +194 -16
  26. package/corpus/templates/content/app/components/editor/database/DatabaseView.tsx +95 -42
  27. package/corpus/templates/content/app/components/editor/database-sources/BuilderSourceReviewDialog.tsx +4 -2
  28. package/corpus/templates/content/app/components/editor/extensions/CodeBlockNode.tsx +10 -166
  29. package/corpus/templates/content/app/components/editor/extensions/VideoBlock.tsx +32 -8
  30. package/corpus/templates/content/app/components/editor/extensions/VideoNode.ts +14 -0
  31. package/corpus/templates/content/app/components/editor/registrySlashItems.ts +5 -1
  32. package/corpus/templates/content/app/hooks/use-content-database.ts +59 -2
  33. package/corpus/templates/content/app/hooks/use-documents.ts +18 -26
  34. package/corpus/templates/content/app/root.tsx +1 -6
  35. package/corpus/templates/content/changelog/2026-07-24-builder-can-now-publish-approved-updates-to-entries-that-are.md +6 -0
  36. package/corpus/templates/content/changelog/2026-07-24-builder-collection-loading-no-longer-restarts-from-the-begin.md +6 -0
  37. package/corpus/templates/content/changelog/2026-07-24-builder-connections-now-complete-securely-from-netlify-deplo.md +6 -0
  38. package/corpus/templates/content/changelog/2026-07-24-builder-draft-creation-now-scopes-execution-to-the-selected-.md +6 -0
  39. package/corpus/templates/content/changelog/2026-07-24-builder-review-now-distinguishes-preparing-a-safe-dry-run-from.md +6 -0
  40. package/corpus/templates/content/changelog/2026-07-24-builder-source-imports-now-recover-from-a-transient-paginati.md +6 -0
  41. package/corpus/templates/content/changelog/2026-07-24-builder-source-refresh-now-preserves-the-provider-bound-fiel.md +6 -0
  42. package/corpus/templates/content/changelog/2026-07-24-builder-update-reviews-now-open-reliably-on-large-collection.md +6 -0
  43. package/corpus/templates/content/changelog/2026-07-24-builder-write-policy-controls-now-reflect-successful-changes.md +6 -0
  44. package/corpus/templates/content/changelog/2026-07-24-date-properties-now-save-reliably-when-selected-with-the-nat.md +6 -0
  45. package/corpus/templates/content/changelog/2026-07-24-editor-slash-search-now-keeps-multi-word-block-names-open.md +6 -0
  46. package/corpus/templates/content/changelog/2026-07-24-exact-editor-slash-commands-now-execute-on-enter-even-if-the.md +6 -0
  47. package/corpus/templates/content/changelog/2026-07-24-new-collaborative-pages-now-save-their-first-edits.md +6 -0
  48. package/corpus/templates/content/changelog/2026-07-24-new-local-builder-rows-no-longer-remain-locked-behind-a-none.md +6 -0
  49. package/corpus/templates/content/changelog/2026-07-24-opening-or-closing-a-collaborative-document-no-longer-lets-u.md +6 -0
  50. package/corpus/templates/content/changelog/2026-07-24-selected-text-now-opens-the-link-editor-with-command-k-or-co.md +6 -0
  51. package/corpus/templates/content/changelog/2026-07-24-slash-menu-block-choices-no-longer-stall-pointer-interaction.md +6 -0
  52. package/corpus/templates/content/changelog/2026-07-24-uploaded-and-embedded-media-now-stay-intact-while-collaborative-document-drafts-save.md +6 -0
  53. package/corpus/templates/content/changelog/2026-07-24-video-link-insertion-now-keeps-its-source-panel-open-while-t.md +6 -0
  54. package/corpus/templates/content/changelog/2026-07-26-builder-publishing-now-shows-safe-validation-details-instead.md +6 -0
  55. package/corpus/templates/content/changelog/2026-07-26-builder-reviews-stay-responsive-on-large-collections-and-pre.md +6 -0
  56. package/corpus/templates/content/changelog/2026-07-26-collaborative-documents-no-longer-flash-duplicated-recent-bl.md +6 -0
  57. package/corpus/templates/content/changelog/2026-07-26-corrected-builder-updates-are-now-reviewed-separately-from-f.md +6 -0
  58. package/corpus/templates/content/changelog/2026-07-26-documents-now-remain-read-only-until-their-live-collaborativ.md +6 -0
  59. package/corpus/templates/content/changelog/2026-07-26-slash-command-menus-now-stay-reachable-when-editing-near-the.md +6 -0
  60. package/corpus/templates/content/changelog/2026-07-26-the-slash-menu-now-shows-one-clear-callout-choice-instead-of.md +6 -0
  61. package/corpus/templates/content/shared/api.ts +1 -0
  62. package/corpus/templates/content/shared/builder-mdx.ts +73 -16
  63. package/corpus/templates/crm/.agents/skills/agent-native-docs/SKILL.md +115 -0
  64. package/corpus/templates/crm/.agents/skills/agent-native-toolkit/SKILL.md +177 -0
  65. package/corpus/templates/crm/.agents/skills/crm/SKILL.md +205 -0
  66. package/corpus/templates/crm/.agents/skills/customizing-agent-native/SKILL.md +220 -0
  67. package/corpus/templates/crm/.agents/skills/feature-flags/SKILL.md +169 -0
  68. package/corpus/templates/crm/.agents/skills/sharing/SKILL.md +228 -0
  69. package/corpus/templates/crm/.agents/skills/storing-data/SKILL.md +178 -0
  70. package/corpus/templates/crm/.agents/skills/upgrade-agent-native/SKILL.md +122 -0
  71. package/corpus/templates/crm/.env.example +5 -0
  72. package/corpus/templates/crm/AGENTS.md +82 -0
  73. package/corpus/templates/crm/CHANGELOG.md +25 -0
  74. package/corpus/templates/crm/DEVELOPING.md +66 -0
  75. package/corpus/templates/crm/README.md +122 -0
  76. package/corpus/templates/crm/_gitignore +12 -0
  77. package/corpus/templates/crm/actions/_crm-action-utils.ts +98 -0
  78. package/corpus/templates/crm/actions/_crm-attribute-utils.ts +219 -0
  79. package/corpus/templates/crm/actions/_crm-dashboard.ts +69 -0
  80. package/corpus/templates/crm/actions/_crm-data-program-actions.ts +15 -0
  81. package/corpus/templates/crm/actions/_crm-list-utils.ts +775 -0
  82. package/corpus/templates/crm/actions/_crm-signal-utils.ts +79 -0
  83. package/corpus/templates/crm/actions/add-crm-record-to-list.ts +164 -0
  84. package/corpus/templates/crm/actions/apply-crm-proposals.ts +291 -0
  85. package/corpus/templates/crm/actions/archive-crm-attribute.ts +69 -0
  86. package/corpus/templates/crm/actions/attach-call-evidence.ts +170 -0
  87. package/corpus/templates/crm/actions/configure-crm-connection.ts +126 -0
  88. package/corpus/templates/crm/actions/configure-native-crm.ts +44 -0
  89. package/corpus/templates/crm/actions/create-crm-attribute.ts +350 -0
  90. package/corpus/templates/crm/actions/create-crm-list.ts +194 -0
  91. package/corpus/templates/crm/actions/create-crm-record.ts +148 -0
  92. package/corpus/templates/crm/actions/create-crm-signal-tracker.ts +58 -0
  93. package/corpus/templates/crm/actions/delete-crm-saved-view.ts +68 -0
  94. package/corpus/templates/crm/actions/delete-staged-dataset.ts +14 -0
  95. package/corpus/templates/crm/actions/estimate-crm-enrichment.ts +229 -0
  96. package/corpus/templates/crm/actions/find-crm-duplicates.ts +87 -0
  97. package/corpus/templates/crm/actions/get-crm-automation-recipe.ts +36 -0
  98. package/corpus/templates/crm/actions/get-crm-dashboard-panel.ts +35 -0
  99. package/corpus/templates/crm/actions/get-crm-dashboard.ts +14 -0
  100. package/corpus/templates/crm/actions/get-crm-overview.ts +13 -0
  101. package/corpus/templates/crm/actions/get-crm-pipeline-data.ts +61 -0
  102. package/corpus/templates/crm/actions/get-crm-record-page.ts +287 -0
  103. package/corpus/templates/crm/actions/get-crm-record.ts +75 -0
  104. package/corpus/templates/crm/actions/get-crm-workspace.ts +364 -0
  105. package/corpus/templates/crm/actions/install-crm-pipeline-dashboard.ts +81 -0
  106. package/corpus/templates/crm/actions/list-crm-attributes.ts +71 -0
  107. package/corpus/templates/crm/actions/list-crm-connections.ts +68 -0
  108. package/corpus/templates/crm/actions/list-crm-dashboard-revisions.ts +15 -0
  109. package/corpus/templates/crm/actions/list-crm-dashboards.ts +15 -0
  110. package/corpus/templates/crm/actions/list-crm-enrichment-slots.ts +28 -0
  111. package/corpus/templates/crm/actions/list-crm-list-entries.ts +192 -0
  112. package/corpus/templates/crm/actions/list-crm-lists.ts +80 -0
  113. package/corpus/templates/crm/actions/list-crm-proposals.ts +55 -0
  114. package/corpus/templates/crm/actions/list-crm-record-field-history.ts +201 -0
  115. package/corpus/templates/crm/actions/list-crm-record-values.ts +200 -0
  116. package/corpus/templates/crm/actions/list-crm-records.ts +103 -0
  117. package/corpus/templates/crm/actions/list-crm-saved-views.ts +66 -0
  118. package/corpus/templates/crm/actions/list-crm-signal-hits.ts +43 -0
  119. package/corpus/templates/crm/actions/list-crm-signal-trackers.ts +32 -0
  120. package/corpus/templates/crm/actions/list-crm-tasks.ts +22 -0
  121. package/corpus/templates/crm/actions/list-staged-datasets.ts +14 -0
  122. package/corpus/templates/crm/actions/list-workspace-connections.ts +47 -0
  123. package/corpus/templates/crm/actions/manage-crm-attribute-option.ts +272 -0
  124. package/corpus/templates/crm/actions/manage-crm-signal-tracker.ts +87 -0
  125. package/corpus/templates/crm/actions/manage-crm-task.ts +132 -0
  126. package/corpus/templates/crm/actions/merge-crm-records.ts +691 -0
  127. package/corpus/templates/crm/actions/navigate.ts +54 -0
  128. package/corpus/templates/crm/actions/provider-api-catalog.ts +22 -0
  129. package/corpus/templates/crm/actions/provider-api-docs.ts +54 -0
  130. package/corpus/templates/crm/actions/provider-api-request.ts +92 -0
  131. package/corpus/templates/crm/actions/query-staged-dataset.ts +44 -0
  132. package/corpus/templates/crm/actions/record-crm-call-insight.ts +160 -0
  133. package/corpus/templates/crm/actions/record-crm-smart-signal.ts +150 -0
  134. package/corpus/templates/crm/actions/remove-crm-list-entry.ts +79 -0
  135. package/corpus/templates/crm/actions/restore-crm-dashboard-revision.ts +23 -0
  136. package/corpus/templates/crm/actions/review-crm-signal.ts +31 -0
  137. package/corpus/templates/crm/actions/run-crm-attribute-fill.ts +443 -0
  138. package/corpus/templates/crm/actions/run-crm-enrichment.ts +647 -0
  139. package/corpus/templates/crm/actions/run-crm-saved-view-program.ts +103 -0
  140. package/corpus/templates/crm/actions/run-crm-signal-trackers.ts +265 -0
  141. package/corpus/templates/crm/actions/run.ts +7 -0
  142. package/corpus/templates/crm/actions/save-crm-dashboard.ts +51 -0
  143. package/corpus/templates/crm/actions/save-crm-saved-view.ts +296 -0
  144. package/corpus/templates/crm/actions/sync-crm.ts +146 -0
  145. package/corpus/templates/crm/actions/update-crm-attribute.ts +111 -0
  146. package/corpus/templates/crm/actions/update-crm-list-entry.ts +102 -0
  147. package/corpus/templates/crm/actions/update-crm-list.ts +97 -0
  148. package/corpus/templates/crm/actions/update-crm-record.ts +543 -0
  149. package/corpus/templates/crm/actions/view-screen.ts +324 -0
  150. package/corpus/templates/crm/agent-native.app-skill.json +69 -0
  151. package/corpus/templates/crm/app/components/crm/CreateCrmRecordDialog.tsx +158 -0
  152. package/corpus/templates/crm/app/components/crm/CrmDashboardPanel.tsx +78 -0
  153. package/corpus/templates/crm/app/components/crm/CrmSignalsPanel.tsx +220 -0
  154. package/corpus/templates/crm/app/components/crm/IntelligenceSettings.tsx +414 -0
  155. package/corpus/templates/crm/app/components/crm/RecordActions.tsx +648 -0
  156. package/corpus/templates/crm/app/components/crm/RecordGrid.tsx +488 -0
  157. package/corpus/templates/crm/app/components/crm/RecordWorkspace.tsx +232 -0
  158. package/corpus/templates/crm/app/components/crm/SavedViewDataProgram.tsx +93 -0
  159. package/corpus/templates/crm/app/components/crm/Surface.tsx +97 -0
  160. package/corpus/templates/crm/app/components/crm/WorkOverview.tsx +180 -0
  161. package/corpus/templates/crm/app/components/crm/board/CrmBoard.tsx +988 -0
  162. package/corpus/templates/crm/app/components/crm/board/board-model.ts +399 -0
  163. package/corpus/templates/crm/app/components/crm/board/entry-filter.ts +150 -0
  164. package/corpus/templates/crm/app/components/crm/board/view-draft.ts +227 -0
  165. package/corpus/templates/crm/app/components/crm/grid/CrmGrid.tsx +1033 -0
  166. package/corpus/templates/crm/app/components/crm/grid/GridCell.tsx +620 -0
  167. package/corpus/templates/crm/app/components/crm/grid/clipboard.ts +84 -0
  168. package/corpus/templates/crm/app/components/crm/grid/model.ts +304 -0
  169. package/corpus/templates/crm/app/components/crm/grid/navigation.ts +220 -0
  170. package/corpus/templates/crm/app/components/crm/grid/query.ts +214 -0
  171. package/corpus/templates/crm/app/components/crm/record/AttributePanel.tsx +192 -0
  172. package/corpus/templates/crm/app/components/crm/record/FieldHistory.tsx +179 -0
  173. package/corpus/templates/crm/app/components/crm/record/RecordHeader.tsx +103 -0
  174. package/corpus/templates/crm/app/components/crm/record/RecordLists.tsx +235 -0
  175. package/corpus/templates/crm/app/components/crm/record/RecordTabs.tsx +206 -0
  176. package/corpus/templates/crm/app/components/crm/record/field-editors.tsx +245 -0
  177. package/corpus/templates/crm/app/components/crm/record/record-data.ts +603 -0
  178. package/corpus/templates/crm/app/components/crm/settings/AdvancedSettings.tsx +49 -0
  179. package/corpus/templates/crm/app/components/crm/settings/AttributeOptionsEditor.tsx +441 -0
  180. package/corpus/templates/crm/app/components/crm/settings/ConnectionSettings.tsx +169 -0
  181. package/corpus/templates/crm/app/components/crm/settings/FieldsSettings.tsx +935 -0
  182. package/corpus/templates/crm/app/components/crm/settings/ListsSettings.tsx +339 -0
  183. package/corpus/templates/crm/app/components/crm/settings/settings-admin.ts +377 -0
  184. package/corpus/templates/crm/app/components/crm/shared/AttributeValueParts.tsx +69 -0
  185. package/corpus/templates/crm/app/components/crm/shared/attribute-value.ts +591 -0
  186. package/corpus/templates/crm/app/components/layout/CrmCommandMenu.tsx +556 -0
  187. package/corpus/templates/crm/app/components/layout/CrmLayout.tsx +130 -0
  188. package/corpus/templates/crm/app/components/layout/CrmSidebar.tsx +407 -0
  189. package/corpus/templates/crm/app/components/layout/command-palette-search.ts +126 -0
  190. package/corpus/templates/crm/app/components/layout/crm-ui-intents.ts +41 -0
  191. package/corpus/templates/crm/app/components/layout/sidebar-collapse.ts +40 -0
  192. package/corpus/templates/crm/app/components/layout/sidebar-lists.ts +109 -0
  193. package/corpus/templates/crm/app/components/ui/alert-dialog.tsx +1 -0
  194. package/corpus/templates/crm/app/components/ui/badge.tsx +1 -0
  195. package/corpus/templates/crm/app/components/ui/button.tsx +1 -0
  196. package/corpus/templates/crm/app/components/ui/card.tsx +1 -0
  197. package/corpus/templates/crm/app/components/ui/checkbox.tsx +1 -0
  198. package/corpus/templates/crm/app/components/ui/dialog.tsx +1 -0
  199. package/corpus/templates/crm/app/components/ui/dropdown-menu.tsx +1 -0
  200. package/corpus/templates/crm/app/components/ui/hover-card.tsx +1 -0
  201. package/corpus/templates/crm/app/components/ui/input.tsx +1 -0
  202. package/corpus/templates/crm/app/components/ui/label.tsx +1 -0
  203. package/corpus/templates/crm/app/components/ui/popover.tsx +1 -0
  204. package/corpus/templates/crm/app/components/ui/select.tsx +1 -0
  205. package/corpus/templates/crm/app/components/ui/sheet.tsx +1 -0
  206. package/corpus/templates/crm/app/components/ui/skeleton.tsx +1 -0
  207. package/corpus/templates/crm/app/components/ui/switch.tsx +1 -0
  208. package/corpus/templates/crm/app/components/ui/table.tsx +1 -0
  209. package/corpus/templates/crm/app/components/ui/tabs.tsx +1 -0
  210. package/corpus/templates/crm/app/components/ui/textarea.tsx +1 -0
  211. package/corpus/templates/crm/app/components/ui/toolkit-provider.tsx +17 -0
  212. package/corpus/templates/crm/app/components/ui/tooltip.tsx +1 -0
  213. package/corpus/templates/crm/app/design-system.ts +3 -0
  214. package/corpus/templates/crm/app/entry.client.tsx +19 -0
  215. package/corpus/templates/crm/app/entry.server.tsx +10 -0
  216. package/corpus/templates/crm/app/global.css +167 -0
  217. package/corpus/templates/crm/app/hooks/use-keyboard-shortcuts.ts +206 -0
  218. package/corpus/templates/crm/app/hooks/use-navigation-state.ts +48 -0
  219. package/corpus/templates/crm/app/i18n/en-US.ts +684 -0
  220. package/corpus/templates/crm/app/i18n/index.ts +34 -0
  221. package/corpus/templates/crm/app/lib/dashboard.ts +13 -0
  222. package/corpus/templates/crm/app/lib/navigation.ts +17 -0
  223. package/corpus/templates/crm/app/lib/tab-id.ts +1 -0
  224. package/corpus/templates/crm/app/lib/types.ts +172 -0
  225. package/corpus/templates/crm/app/lib/utils.ts +1 -0
  226. package/corpus/templates/crm/app/root.tsx +93 -0
  227. package/corpus/templates/crm/app/routes/_index.tsx +26 -0
  228. package/corpus/templates/crm/app/routes/accounts.tsx +41 -0
  229. package/corpus/templates/crm/app/routes/agent.tsx +5 -0
  230. package/corpus/templates/crm/app/routes/ask.tsx +53 -0
  231. package/corpus/templates/crm/app/routes/dashboard.tsx +118 -0
  232. package/corpus/templates/crm/app/routes/lists.tsx +228 -0
  233. package/corpus/templates/crm/app/routes/opportunities.tsx +41 -0
  234. package/corpus/templates/crm/app/routes/people.tsx +41 -0
  235. package/corpus/templates/crm/app/routes/proposals.tsx +388 -0
  236. package/corpus/templates/crm/app/routes/records.$recordId.tsx +53 -0
  237. package/corpus/templates/crm/app/routes/records.tsx +64 -0
  238. package/corpus/templates/crm/app/routes/settings.tsx +118 -0
  239. package/corpus/templates/crm/app/routes/setup.tsx +308 -0
  240. package/corpus/templates/crm/app/routes/tasks.tsx +252 -0
  241. package/corpus/templates/crm/app/routes/views.tsx +683 -0
  242. package/corpus/templates/crm/app/routes.ts +19 -0
  243. package/corpus/templates/crm/app/vite-env.d.ts +6 -0
  244. 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
  245. package/corpus/templates/crm/changelog/2026-07-26-crm-now-finds-likely-duplicate-records-and-tells-you-exactly.md +6 -0
  246. package/corpus/templates/crm/changelog/2026-07-26-enrichment-now-shows-what-a-run-will-cost-before-it-starts-g.md +6 -0
  247. package/corpus/templates/crm/changelog/2026-07-26-lists-turn-any-set-of-records-into-a-pipeline-with-its-own-s.md +6 -0
  248. package/corpus/templates/crm/changelog/2026-07-26-moving-a-record-or-list-entry-into-a-stage-that-was-retired-.md +6 -0
  249. package/corpus/templates/crm/changelog/2026-07-26-opening-a-record-now-shows-one-page-with-its-fields-the-list.md +6 -0
  250. 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
  251. package/corpus/templates/crm/changelog/2026-07-26-provider-record-edits-now-show-an-exact-before-after-diff-an.md +6 -0
  252. package/corpus/templates/crm/changelog/2026-07-26-record-page-attribute-labels-now-read-in-full-instead-of-bei.md +6 -0
  253. package/corpus/templates/crm/changelog/2026-07-26-records-now-use-typed-fields-currency-dates-ratings-status-s.md +6 -0
  254. package/corpus/templates/crm/changelog/2026-07-26-retyping-a-value-on-the-record-page-now-replaces-it-instead-.md +6 -0
  255. package/corpus/templates/crm/changelog/2026-07-26-settings-now-has-fields-and-lists-tabs-for-adding-fields-edi.md +6 -0
  256. package/corpus/templates/crm/changelog/2026-07-26-the-proposal-review-now-shows-the-field-that-actually-change.md +6 -0
  257. package/corpus/templates/crm/changelog/2026-07-26-the-record-grid-is-now-a-real-spreadsheet-filter-sort-and-pa.md +6 -0
  258. package/corpus/templates/crm/components.json +20 -0
  259. package/corpus/templates/crm/docs/architecture/crm-contract.md +214 -0
  260. package/corpus/templates/crm/docs/features/README.md +55 -0
  261. package/corpus/templates/crm/docs/features/f1-records-and-fields.mdx +52 -0
  262. package/corpus/templates/crm/docs/features/f2-lists-pipelines-and-views.mdx +48 -0
  263. package/corpus/templates/crm/docs/features/f3-work-and-record-quality.mdx +40 -0
  264. package/corpus/templates/crm/docs/features/f4-evidence-and-chat.mdx +47 -0
  265. package/corpus/templates/crm/learnings.defaults.md +5 -0
  266. package/corpus/templates/crm/netlify.toml +11 -0
  267. package/corpus/templates/crm/package.json +61 -0
  268. package/corpus/templates/crm/public/favicon.svg +1 -0
  269. package/corpus/templates/crm/public/icon-180.svg +1 -0
  270. package/corpus/templates/crm/public/manifest.json +11 -0
  271. package/corpus/templates/crm/react-router.config.ts +7 -0
  272. package/corpus/templates/crm/server/crm/adapter.ts +32 -0
  273. package/corpus/templates/crm/server/crm/crm-field-firewall.ts +64 -0
  274. package/corpus/templates/crm/server/crm/crm-mirror.ts +721 -0
  275. package/corpus/templates/crm/server/crm/field-policy-attributes.ts +48 -0
  276. package/corpus/templates/crm/server/crm/hubspot-adapter.ts +1146 -0
  277. package/corpus/templates/crm/server/crm/native-adapter.ts +1810 -0
  278. package/corpus/templates/crm/server/crm/provider-record-link.ts +264 -0
  279. package/corpus/templates/crm/server/crm/read-through.ts +207 -0
  280. package/corpus/templates/crm/server/crm/salesforce-adapter.ts +1255 -0
  281. package/corpus/templates/crm/server/db/crm-store.ts +1112 -0
  282. package/corpus/templates/crm/server/db/index.ts +191 -0
  283. package/corpus/templates/crm/server/db/schema.ts +647 -0
  284. package/corpus/templates/crm/server/lib/crm-query.ts +1558 -0
  285. package/corpus/templates/crm/server/lib/dedupe.ts +714 -0
  286. package/corpus/templates/crm/server/lib/enrichment-cost.ts +381 -0
  287. package/corpus/templates/crm/server/lib/enrichment-slots.ts +519 -0
  288. package/corpus/templates/crm/server/lib/intelligence/default-detectors.ts +65 -0
  289. package/corpus/templates/crm/server/lib/intelligence/evidence.ts +105 -0
  290. package/corpus/templates/crm/server/lib/intelligence/keyword-detector.ts +94 -0
  291. package/corpus/templates/crm/server/lib/intelligence/smart-detector.ts +39 -0
  292. package/corpus/templates/crm/server/lib/intelligence/summary.ts +19 -0
  293. package/corpus/templates/crm/server/lib/lifecycle.ts +634 -0
  294. package/corpus/templates/crm/server/lib/provider-api.ts +64 -0
  295. package/corpus/templates/crm/server/middleware/auth.ts +4 -0
  296. package/corpus/templates/crm/server/plugins/agent-chat.ts +65 -0
  297. package/corpus/templates/crm/server/plugins/auth.ts +14 -0
  298. package/corpus/templates/crm/server/plugins/core-routes.ts +17 -0
  299. package/corpus/templates/crm/server/plugins/db.ts +738 -0
  300. package/corpus/templates/crm/server/routes/[...page].get.ts +5 -0
  301. package/corpus/templates/crm/shared/crm-attributes.ts +469 -0
  302. package/corpus/templates/crm/shared/crm-automation-recipes.ts +132 -0
  303. package/corpus/templates/crm/shared/crm-contract.ts +417 -0
  304. package/corpus/templates/crm/shared/crm-navigation.ts +190 -0
  305. package/corpus/templates/crm/shared/crm-sales-config.ts +63 -0
  306. package/corpus/templates/crm/ssr-entry.ts +11 -0
  307. package/corpus/templates/crm/tsconfig.json +24 -0
  308. package/corpus/templates/crm/vite.config.ts +15 -0
  309. package/corpus/templates/crm/vitest.config.ts +5 -0
  310. package/corpus/templates/design/app/i18n-data.ts +12 -12
  311. package/corpus/templates/design/changelog/2026-07-26-apply-design-updates-from-a-centered-top-toolbar-without-cov.md +6 -0
  312. package/corpus/toolkit/CHANGELOG.md +6 -0
  313. package/corpus/toolkit/package.json +1 -1
  314. package/corpus/toolkit/src/editor/useCollabReconcile.ts +14 -1
  315. package/dist/cli/templates-meta.d.ts.map +1 -1
  316. package/dist/cli/templates-meta.js +12 -0
  317. package/dist/cli/templates-meta.js.map +1 -1
  318. package/dist/collab/routes.d.ts +2 -2
  319. package/dist/deploy/build.js +6 -0
  320. package/dist/deploy/build.js.map +1 -1
  321. package/dist/observability/routes.d.ts +3 -3
  322. package/dist/server/builder-browser.d.ts +13 -0
  323. package/dist/server/builder-browser.d.ts.map +1 -1
  324. package/dist/server/builder-browser.js +59 -0
  325. package/dist/server/builder-browser.js.map +1 -1
  326. package/dist/server/core-routes-plugin.d.ts.map +1 -1
  327. package/dist/server/core-routes-plugin.js +10 -5
  328. package/dist/server/core-routes-plugin.js.map +1 -1
  329. package/dist/server/transcribe-voice.d.ts +1 -1
  330. package/package.json +2 -2
  331. package/src/cli/templates-meta.ts +12 -0
  332. package/src/deploy/build.ts +8 -0
  333. package/src/server/builder-browser.ts +76 -0
  334. package/src/server/core-routes-plugin.ts +15 -5
@@ -0,0 +1,170 @@
1
+ import { defineAction } from "@agent-native/core/action";
2
+ import { assertAccess } from "@agent-native/core/sharing";
3
+ import { and, eq, inArray } from "drizzle-orm";
4
+ import { z } from "zod";
5
+
6
+ import { isSafeCrmEvidenceExcerpt } from "../server/crm/crm-field-firewall.js";
7
+ import { getDb, schema } from "../server/db/index.js";
8
+ import { isDurableClipsEvidenceUrl } from "../shared/crm-automation-recipes.js";
9
+ import { requireCrmScope } from "./_crm-action-utils.js";
10
+
11
+ const httpsUrl = z
12
+ .string()
13
+ .url()
14
+ .max(2_048)
15
+ .refine(
16
+ (value) => /^https:\/\//i.test(value),
17
+ "sourceUrl must be an https URL",
18
+ );
19
+
20
+ export default defineAction({
21
+ description:
22
+ "Attach bounded call evidence to a CRM record. Store only an artifact reference, URL, optional short quote, speaker, timestamp, and summary; never submit a transcript or media payload.",
23
+ schema: z
24
+ .object({
25
+ recordId: z.string().trim().min(1).max(128).optional(),
26
+ recordIds: z
27
+ .array(z.string().trim().min(1).max(128))
28
+ .min(1)
29
+ .max(20)
30
+ .optional(),
31
+ interactionId: z.string().trim().min(1).max(128).optional(),
32
+ artifactId: z.string().trim().min(1).max(256),
33
+ sourceUrl: httpsUrl,
34
+ sourceApp: z.string().trim().min(1).max(80).default("clips"),
35
+ artifactType: z.string().trim().min(1).max(80).default("call-evidence"),
36
+ quote: z
37
+ .string()
38
+ .trim()
39
+ .max(1_200)
40
+ .refine(
41
+ isSafeCrmEvidenceExcerpt,
42
+ "quote must be a bounded human-readable excerpt, not transcript, media, binary, base64, or data-url content",
43
+ )
44
+ .optional(),
45
+ speaker: z.string().trim().max(240).optional(),
46
+ startSeconds: z.number().finite().min(0).max(86_400).optional(),
47
+ endSeconds: z.number().finite().min(0).max(86_400).optional(),
48
+ summary: z
49
+ .string()
50
+ .trim()
51
+ .max(2_000)
52
+ .refine(
53
+ isSafeCrmEvidenceExcerpt,
54
+ "summary must be a bounded human-readable excerpt, not transcript, media, binary, base64, or data-url content",
55
+ )
56
+ .optional(),
57
+ capturedAt: z.string().datetime({ offset: true }).optional(),
58
+ })
59
+ .superRefine((value, issue) => {
60
+ if (
61
+ (!value.recordId && !value.recordIds) ||
62
+ (value.recordId && value.recordIds)
63
+ ) {
64
+ issue.addIssue({
65
+ code: "custom",
66
+ message: "Provide exactly one of recordId or recordIds",
67
+ path: ["recordId"],
68
+ });
69
+ }
70
+ if (
71
+ value.endSeconds !== undefined &&
72
+ value.startSeconds !== undefined &&
73
+ value.endSeconds < value.startSeconds
74
+ ) {
75
+ issue.addIssue({
76
+ code: "custom",
77
+ message: "endSeconds must be after startSeconds",
78
+ path: ["endSeconds"],
79
+ });
80
+ }
81
+ if (
82
+ value.sourceApp.trim().toLowerCase() === "clips" &&
83
+ !isDurableClipsEvidenceUrl(value.sourceUrl)
84
+ ) {
85
+ issue.addIssue({
86
+ code: "custom",
87
+ message:
88
+ "Clips evidence must use a durable /share/<id> or /r/<id> page URL without an access token, media endpoint, or transcript fragment",
89
+ path: ["sourceUrl"],
90
+ });
91
+ }
92
+ }),
93
+ audit: {
94
+ target: (args, result) => {
95
+ const value = result as {
96
+ evidence?: Array<{
97
+ ownerEmail: string;
98
+ orgId: string | null;
99
+ visibility: "private" | "org";
100
+ }>;
101
+ ownerEmail: string;
102
+ orgId: string | null;
103
+ visibility: "private" | "org";
104
+ };
105
+ const evidence = value.evidence?.[0] ?? value;
106
+ return {
107
+ type: "crm-record",
108
+ id: args.recordId ?? args.recordIds?.[0] ?? "unknown",
109
+ ownerEmail: evidence.ownerEmail,
110
+ orgId: evidence.orgId,
111
+ visibility: evidence.visibility,
112
+ };
113
+ },
114
+ summary: (args) =>
115
+ `Attached call evidence to ${args.recordIds?.length ?? 1} CRM record${args.recordIds?.length === 1 ? "" : "s"}`,
116
+ recordInputs: false,
117
+ },
118
+ run: async (args, ctx) => {
119
+ const recordIds = args.recordId
120
+ ? [args.recordId]
121
+ : [...new Set(args.recordIds ?? [])];
122
+ await Promise.all(
123
+ recordIds.map((recordId) =>
124
+ assertAccess("crm-record", recordId, "editor"),
125
+ ),
126
+ );
127
+ if (args.interactionId)
128
+ await assertAccess("crm-interaction", args.interactionId, "viewer");
129
+ const db = getDb();
130
+ const scope = requireCrmScope(ctx);
131
+ const now = new Date().toISOString();
132
+ const rows = recordIds.map((recordId) => ({
133
+ id: crypto.randomUUID(),
134
+ interactionId: args.interactionId ?? null,
135
+ recordId,
136
+ sourceApp: args.sourceApp,
137
+ artifactType: args.artifactType,
138
+ artifactId: args.artifactId,
139
+ sourceUrl: args.sourceUrl,
140
+ quote: args.quote ?? "",
141
+ speaker: args.speaker ?? null,
142
+ startSeconds: args.startSeconds ?? null,
143
+ endSeconds: args.endSeconds ?? null,
144
+ summary: args.summary ?? "",
145
+ capturedAt: args.capturedAt ?? now,
146
+ ...scope,
147
+ createdAt: now,
148
+ updatedAt: now,
149
+ }));
150
+ await db.transaction(async (tx) => {
151
+ await tx.insert(schema.crmCallEvidence).values(rows);
152
+ });
153
+ const evidence = await db
154
+ .select()
155
+ .from(schema.crmCallEvidence)
156
+ .where(
157
+ and(
158
+ inArray(
159
+ schema.crmCallEvidence.id,
160
+ rows.map((row) => row.id),
161
+ ),
162
+ eq(schema.crmCallEvidence.ownerEmail, scope.ownerEmail),
163
+ ),
164
+ )
165
+ .limit(recordIds.length);
166
+ if (evidence.length !== recordIds.length)
167
+ throw new Error("Call evidence could not be verified after saving.");
168
+ return args.recordId ? evidence[0] : { evidence };
169
+ },
170
+ });
@@ -0,0 +1,126 @@
1
+ import { defineAction, type ActionRunContext } from "@agent-native/core/action";
2
+ import { accessFilter, assertAccess } from "@agent-native/core/sharing";
3
+ import { and, eq } from "drizzle-orm";
4
+ import { z } from "zod";
5
+
6
+ import { createConnectedCrmAdapter } from "../server/crm/adapter.js";
7
+ import { getDb, schema } from "../server/db/index.js";
8
+ import { requireCrmScope, toJson } from "./_crm-action-utils.js";
9
+
10
+ const idList = z.array(z.string().trim().min(1).max(160)).max(50).default([]);
11
+
12
+ export default defineAction({
13
+ description:
14
+ "Register an authorized HubSpot or Salesforce workspace Connection as a scoped CRM companion connection. Provider credentials remain in workspace Connections.",
15
+ schema: z.object({
16
+ provider: z.enum(["hubspot", "salesforce"]).default("hubspot"),
17
+ workspaceConnectionId: z.string().trim().min(1).max(160).optional(),
18
+ label: z.string().trim().min(1).max(160).optional(),
19
+ mode: z.enum(["connected", "hybrid"]).default("connected"),
20
+ selectedPipelineIds: idList,
21
+ selectedObjectTypes: z
22
+ .array(z.string().trim().min(1).max(120))
23
+ .min(1)
24
+ .max(50)
25
+ .optional(),
26
+ }),
27
+ audit: {
28
+ target: (_args, result) => {
29
+ const connection = result as {
30
+ id: string;
31
+ ownerEmail: string;
32
+ orgId: string | null;
33
+ visibility: "private" | "org";
34
+ };
35
+ return {
36
+ type: "crm-connection",
37
+ id: connection.id,
38
+ ownerEmail: connection.ownerEmail,
39
+ orgId: connection.orgId,
40
+ visibility: connection.visibility,
41
+ };
42
+ },
43
+ summary: (_args, result) =>
44
+ `Configured CRM connection ${(result as { id?: string })?.id ?? ""}`,
45
+ recordInputs: false,
46
+ },
47
+ run: async (args, ctx?: ActionRunContext) => {
48
+ const scope = requireCrmScope(ctx);
49
+ const adapter = await createConnectedCrmAdapter({
50
+ provider: args.provider,
51
+ connectionId: args.workspaceConnectionId,
52
+ userEmail: scope.ownerEmail,
53
+ orgId: scope.orgId,
54
+ });
55
+ const id = adapter.connection.connectionId;
56
+ const db = getDb();
57
+ const [existing] = await db
58
+ .select({ id: schema.crmConnections.id })
59
+ .from(schema.crmConnections)
60
+ .where(
61
+ and(
62
+ eq(schema.crmConnections.workspaceConnectionId, id),
63
+ accessFilter(schema.crmConnections, schema.crmConnectionShares),
64
+ ),
65
+ )
66
+ .limit(1);
67
+ const now = new Date().toISOString();
68
+ const selectedObjectTypes =
69
+ args.selectedObjectTypes ??
70
+ (args.provider === "hubspot"
71
+ ? ["companies", "contacts", "deals"]
72
+ : ["Account", "Contact", "Opportunity"]);
73
+ const accessScopeKey = `workspace:${id}:${adapter.connection.actorId ?? scope.ownerEmail}`;
74
+ const values = {
75
+ provider: args.provider,
76
+ workspaceConnectionId: id,
77
+ label:
78
+ args.label ?? (args.provider === "hubspot" ? "HubSpot" : "Salesforce"),
79
+ accountId: adapter.connection.accountId ?? null,
80
+ mode: args.mode,
81
+ status: "connected" as const,
82
+ selectedPipelinesJson: toJson(args.selectedPipelineIds, 8_000),
83
+ selectedObjectTypesJson: toJson(selectedObjectTypes, 8_000),
84
+ accessScopeKey,
85
+ accessScopeJson: toJson(
86
+ {
87
+ key: accessScopeKey,
88
+ actorId: adapter.connection.actorId,
89
+ mode: adapter.connection.actorId ? "user" : "service-account",
90
+ recordVisibility: adapter.connection.actorId ? "actor" : "unknown",
91
+ },
92
+ 4_000,
93
+ ),
94
+ lastError: null,
95
+ updatedAt: now,
96
+ };
97
+ const localId = existing?.id ?? id;
98
+
99
+ if (existing) {
100
+ await assertAccess("crm-connection", existing.id, "editor");
101
+ await db
102
+ .update(schema.crmConnections)
103
+ .set(values)
104
+ .where(
105
+ and(
106
+ eq(schema.crmConnections.id, existing.id),
107
+ accessFilter(
108
+ schema.crmConnections,
109
+ schema.crmConnectionShares,
110
+ undefined,
111
+ "editor",
112
+ ),
113
+ ),
114
+ );
115
+ } else {
116
+ await db.insert(schema.crmConnections).values({
117
+ id: localId,
118
+ ...values,
119
+ ...scope,
120
+ createdAt: now,
121
+ });
122
+ }
123
+
124
+ return { id: localId, ...scope, provider: args.provider };
125
+ },
126
+ });
@@ -0,0 +1,44 @@
1
+ import { defineAction, type ActionRunContext } from "@agent-native/core/action";
2
+ import { z } from "zod";
3
+
4
+ import { configureNativeCrmConnection } from "../server/crm/native-adapter.js";
5
+ import { requireCrmScope } from "./_crm-action-utils.js";
6
+
7
+ export default defineAction({
8
+ description:
9
+ "Initialize a local-authoritative Native SQL CRM with accounts, people, and opportunities. It requires no provider connection or credential and uses the configured SQLite, Postgres, or D1 database.",
10
+ schema: z.object({
11
+ label: z.string().trim().min(1).max(160).optional(),
12
+ }),
13
+ audit: {
14
+ target: (_args, result) => {
15
+ const connection = result as {
16
+ id: string;
17
+ ownerEmail: string;
18
+ orgId: string | null;
19
+ visibility: "private" | "org";
20
+ };
21
+ return {
22
+ type: "crm-connection",
23
+ id: connection.id,
24
+ ownerEmail: connection.ownerEmail,
25
+ orgId: connection.orgId,
26
+ visibility: connection.visibility,
27
+ };
28
+ },
29
+ summary: (_args, result) =>
30
+ `Configured Native SQL CRM ${(result as { id?: string })?.id ?? ""}`,
31
+ recordInputs: false,
32
+ },
33
+ run: async (args, ctx?: ActionRunContext) => {
34
+ const result = await configureNativeCrmConnection({
35
+ label: args.label,
36
+ ownership: requireCrmScope(ctx),
37
+ });
38
+ return {
39
+ ...result,
40
+ provider: "native" as const,
41
+ mode: "native" as const,
42
+ };
43
+ },
44
+ });
@@ -0,0 +1,350 @@
1
+ import { defineAction, type ActionRunContext } from "@agent-native/core/action";
2
+ import { accessFilter, assertAccess } from "@agent-native/core/sharing";
3
+ import { and, eq } from "drizzle-orm";
4
+ import { z } from "zod";
5
+
6
+ import { getDb, schema } from "../server/db/index.js";
7
+ import {
8
+ ATTRIBUTE_TYPE_SPECS,
9
+ CRM_ATTRIBUTE_TYPES,
10
+ } from "../shared/crm-attributes.js";
11
+ import { requireCrmScope, toJson } from "./_crm-action-utils.js";
12
+ import {
13
+ CrmAttributeValueError,
14
+ legacyValueTypeFor,
15
+ loadAttributeOptions,
16
+ requireAttributeType,
17
+ requireEditableAttribute,
18
+ toApiSlug,
19
+ toAttributeDefinition,
20
+ } from "./_crm-attribute-utils.js";
21
+
22
+ const MAX_CONFIG_CHARS = 4000;
23
+
24
+ const optionInput = z.object({
25
+ value: z.string().trim().min(1).max(200),
26
+ title: z.string().trim().min(1).max(200).optional(),
27
+ color: z.string().trim().min(1).max(64).optional(),
28
+ targetDays: z.number().int().min(0).max(3650).nullish(),
29
+ celebrate: z.boolean().optional(),
30
+ });
31
+
32
+ export default defineAction({
33
+ description:
34
+ "Create one local, typed CRM attribute on an object type or a list. The attribute is always local-authoritative — never provider-owned — and its snake_case api slug is derived from the title and immutable afterwards. Supply options only for status and select attributes; status options may carry targetDays (stage SLA) and celebrate. The system-only types interaction and personal-name are rejected.",
35
+ schema: z.object({
36
+ target: z.enum(["object", "list"]).default("object"),
37
+ targetId: z
38
+ .string()
39
+ .trim()
40
+ .min(1)
41
+ .max(200)
42
+ .describe(
43
+ "The object type for target=object (for example accounts), or the list id for target=list.",
44
+ ),
45
+ connectionId: z.string().trim().min(1).max(128).optional(),
46
+ title: z.string().trim().min(1).max(200),
47
+ type: z.enum(CRM_ATTRIBUTE_TYPES),
48
+ multi: z
49
+ .boolean()
50
+ .default(false)
51
+ .describe(
52
+ "The only cardinality knob, including for record-reference attributes.",
53
+ ),
54
+ description: z.string().trim().max(2000).optional(),
55
+ config: z
56
+ .record(z.string(), z.unknown())
57
+ .optional()
58
+ .describe(
59
+ 'Type configuration, for example {"currency":{"code":"USD"}} or {"reference":{"allowedObjectTypes":["accounts"]}}.',
60
+ ),
61
+ required: z.boolean().default(false),
62
+ uniqueValue: z.boolean().default(false),
63
+ historyTracked: z
64
+ .boolean()
65
+ .default(true)
66
+ .describe(
67
+ "Set false only for churny mirror fields whose history is not worth keeping.",
68
+ ),
69
+ position: z.number().int().min(0).max(10_000).optional(),
70
+ inverseAttributeId: z
71
+ .string()
72
+ .trim()
73
+ .min(1)
74
+ .max(128)
75
+ .optional()
76
+ .describe(
77
+ "record-reference only: pairs this attribute with the reverse-direction attribute on the other object.",
78
+ ),
79
+ options: z.array(optionInput).max(200).optional(),
80
+ }),
81
+ audit: {
82
+ target: (_args, result) => {
83
+ const attribute = result as {
84
+ id: string;
85
+ ownerEmail: string;
86
+ orgId: string | null;
87
+ visibility: "private" | "org";
88
+ };
89
+ return {
90
+ type: "crm-field-policy",
91
+ id: attribute.id,
92
+ ownerEmail: attribute.ownerEmail,
93
+ orgId: attribute.orgId,
94
+ visibility: attribute.visibility,
95
+ };
96
+ },
97
+ summary: (args) =>
98
+ `Created CRM ${args.type} attribute ${args.title} on ${args.target} ${args.targetId}`,
99
+ },
100
+ run: async (args, ctx?: ActionRunContext) => {
101
+ const ownership = requireCrmScope(ctx);
102
+ requireAttributeType(args.type);
103
+ const spec = ATTRIBUTE_TYPE_SPECS[args.type];
104
+
105
+ if (args.multi && !spec.supportsMulti) {
106
+ throw new CrmAttributeValueError(
107
+ "crm-attribute-cardinality",
108
+ `Attribute type "${args.type}" cannot be multi-valued.`,
109
+ );
110
+ }
111
+ const optionInputs = args.options ?? [];
112
+ if (optionInputs.length > 0 && !spec.usesOptions) {
113
+ throw new CrmAttributeValueError(
114
+ "crm-attribute-options-unsupported",
115
+ `Attribute type "${args.type}" does not use managed options. Only status and select attributes take options.`,
116
+ );
117
+ }
118
+ if (args.type !== "status") {
119
+ const stageOnly = optionInputs.find(
120
+ (option) => option.targetDays != null || option.celebrate === true,
121
+ );
122
+ if (stageOnly) {
123
+ throw new CrmAttributeValueError(
124
+ "crm-attribute-option-stage-fields",
125
+ `targetDays and celebrate describe a pipeline stage and apply to status attributes only; option "${stageOnly.value}" is on a ${args.type} attribute.`,
126
+ );
127
+ }
128
+ }
129
+ const duplicate = optionInputs.find(
130
+ (option, index) =>
131
+ optionInputs.findIndex((other) => other.value === option.value) !==
132
+ index,
133
+ );
134
+ if (duplicate) {
135
+ throw new CrmAttributeValueError(
136
+ "crm-attribute-option-duplicate",
137
+ `Option value "${duplicate.value}" was supplied more than once.`,
138
+ );
139
+ }
140
+ if (args.inverseAttributeId && args.type !== "record-reference") {
141
+ throw new CrmAttributeValueError(
142
+ "crm-attribute-inverse-type",
143
+ `inverseAttributeId pairs two record-reference attributes; this attribute is a ${args.type}.`,
144
+ );
145
+ }
146
+
147
+ const db = getDb();
148
+ const connectionId = await resolveConnectionId(args);
149
+
150
+ // Deliberately not access-filtered: the unique index spans every row for
151
+ // (connection_id, object_type, field_name), so a slug taken by a row this
152
+ // caller cannot see still collides. Reporting that as a clean 422 beats a
153
+ // raw constraint violation.
154
+ const siblings = await db
155
+ .select({
156
+ id: schema.crmFieldPolicies.id,
157
+ fieldName: schema.crmFieldPolicies.fieldName,
158
+ position: schema.crmFieldPolicies.position,
159
+ })
160
+ .from(schema.crmFieldPolicies)
161
+ .where(
162
+ and(
163
+ eq(schema.crmFieldPolicies.connectionId, connectionId),
164
+ eq(schema.crmFieldPolicies.objectType, args.targetId),
165
+ ),
166
+ );
167
+
168
+ const apiSlug = toApiSlug(args.title);
169
+ if (siblings.some((sibling) => sibling.fieldName === apiSlug)) {
170
+ throw new CrmAttributeValueError(
171
+ "crm-attribute-slug-taken",
172
+ `An attribute with the slug "${apiSlug}" already exists on ${args.target} ${args.targetId}. Slugs are immutable, so pick a different title.`,
173
+ );
174
+ }
175
+
176
+ const inverse = args.inverseAttributeId
177
+ ? await loadPairableInverse(args.inverseAttributeId)
178
+ : null;
179
+
180
+ const id = crypto.randomUUID();
181
+ const now = new Date().toISOString();
182
+ await db.insert(schema.crmFieldPolicies).values({
183
+ id,
184
+ connectionId,
185
+ // Both columns carry the target: `object_type` keeps the legacy unique
186
+ // index meaningful for list attributes too.
187
+ objectType: args.targetId,
188
+ targetId: args.targetId,
189
+ target: args.target,
190
+ fieldName: apiSlug,
191
+ apiSlug,
192
+ label: args.title,
193
+ description: args.description ?? null,
194
+ valueType: legacyValueTypeFor(args.type, args.multi),
195
+ // An authored attribute is owned here, never by a provider, even when the
196
+ // connection mirrors HubSpot or Salesforce.
197
+ storagePolicy: "local-authoritative",
198
+ authority: "local-authoritative",
199
+ attributeType: args.type,
200
+ multi: args.multi,
201
+ historyTracked: args.historyTracked,
202
+ inverseAttributeId: inverse?.id ?? null,
203
+ configJson: toJson(args.config ?? {}, MAX_CONFIG_CHARS),
204
+ uniqueValue: args.uniqueValue,
205
+ required: args.required,
206
+ readable: true,
207
+ createable: true,
208
+ updateable: true,
209
+ position:
210
+ args.position ??
211
+ siblings.reduce((max, sibling) => Math.max(max, sibling.position), -1) +
212
+ 1,
213
+ createdAt: now,
214
+ updatedAt: now,
215
+ ...ownership,
216
+ });
217
+
218
+ if (optionInputs.length > 0) {
219
+ await db.insert(schema.crmAttributeOptions).values(
220
+ optionInputs.map((option, index) => ({
221
+ id: crypto.randomUUID(),
222
+ attributeId: id,
223
+ value: option.value,
224
+ title: option.title ?? option.value,
225
+ color: option.color ?? null,
226
+ position: index,
227
+ targetDays: option.targetDays ?? null,
228
+ celebrate: option.celebrate ?? false,
229
+ createdAt: now,
230
+ updatedAt: now,
231
+ ...ownership,
232
+ })),
233
+ );
234
+ }
235
+
236
+ if (inverse) {
237
+ await db
238
+ .update(schema.crmFieldPolicies)
239
+ .set({ inverseAttributeId: id, updatedAt: now })
240
+ .where(
241
+ and(
242
+ eq(schema.crmFieldPolicies.id, inverse.id),
243
+ accessFilter(
244
+ schema.crmFieldPolicies,
245
+ schema.crmFieldPolicyShares,
246
+ undefined,
247
+ "editor",
248
+ ),
249
+ ),
250
+ );
251
+ }
252
+
253
+ const [row] = await db
254
+ .select()
255
+ .from(schema.crmFieldPolicies)
256
+ .where(eq(schema.crmFieldPolicies.id, id))
257
+ .limit(1);
258
+ if (!row) {
259
+ throw new Error("CRM attribute could not be verified after creating it.");
260
+ }
261
+ const options = await loadAttributeOptions({
262
+ attributeIds: [id],
263
+ includeArchived: true,
264
+ });
265
+ return {
266
+ ...toAttributeDefinition(row, options.get(id) ?? []),
267
+ ...ownership,
268
+ };
269
+ },
270
+ });
271
+
272
+ /**
273
+ * The connection an attribute belongs to. A list already names one, so a list
274
+ * attribute never has to be told which connection it is on.
275
+ */
276
+ async function resolveConnectionId(args: {
277
+ target: "object" | "list";
278
+ targetId: string;
279
+ connectionId?: string;
280
+ }): Promise<string> {
281
+ const db = getDb();
282
+ if (args.target === "list") {
283
+ await assertAccess("crm-list", args.targetId, "editor");
284
+ const [list] = await db
285
+ .select({ connectionId: schema.crmLists.connectionId })
286
+ .from(schema.crmLists)
287
+ .where(eq(schema.crmLists.id, args.targetId))
288
+ .limit(1);
289
+ if (!list) {
290
+ throw new CrmAttributeValueError(
291
+ "crm-list-not-found",
292
+ `CRM list ${args.targetId} was not found.`,
293
+ );
294
+ }
295
+ if (args.connectionId && args.connectionId !== list.connectionId) {
296
+ throw new CrmAttributeValueError(
297
+ "crm-attribute-connection-mismatch",
298
+ `List ${args.targetId} belongs to connection ${list.connectionId}, not ${args.connectionId}.`,
299
+ );
300
+ }
301
+ return list.connectionId;
302
+ }
303
+
304
+ const connections = await db
305
+ .select({ id: schema.crmConnections.id })
306
+ .from(schema.crmConnections)
307
+ .where(
308
+ and(
309
+ ...(args.connectionId
310
+ ? [eq(schema.crmConnections.id, args.connectionId)]
311
+ : []),
312
+ accessFilter(
313
+ schema.crmConnections,
314
+ schema.crmConnectionShares,
315
+ undefined,
316
+ "editor",
317
+ ),
318
+ ),
319
+ )
320
+ .limit(args.connectionId ? 1 : 2);
321
+ if (!connections.length) {
322
+ throw new Error(
323
+ "No CRM connection is available with editor access. Configure a Native SQL CRM or a provider connection first.",
324
+ );
325
+ }
326
+ if (!args.connectionId && connections.length > 1) {
327
+ throw new Error(
328
+ "More than one CRM connection is available. Provide connectionId to choose one.",
329
+ );
330
+ }
331
+ return connections[0]!.id;
332
+ }
333
+
334
+ /** The other half of a paired relationship, or a typed error explaining why not. */
335
+ async function loadPairableInverse(inverseAttributeId: string) {
336
+ const inverse = await requireEditableAttribute(inverseAttributeId);
337
+ if (inverse.attributeType !== "record-reference") {
338
+ throw new CrmAttributeValueError(
339
+ "crm-attribute-inverse-type",
340
+ `Attribute ${inverseAttributeId} is a ${inverse.attributeType}; only a record-reference attribute can be the inverse side.`,
341
+ );
342
+ }
343
+ if (inverse.inverseAttributeId) {
344
+ throw new CrmAttributeValueError(
345
+ "crm-attribute-inverse-taken",
346
+ `Attribute ${inverseAttributeId} is already paired with ${inverse.inverseAttributeId}. Unpair it before pairing it again.`,
347
+ );
348
+ }
349
+ return inverse;
350
+ }