@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,339 @@
1
+ import {
2
+ useActionMutation,
3
+ useActionQuery,
4
+ } from "@agent-native/core/client/hooks";
5
+ import { useT } from "@agent-native/core/client/i18n";
6
+ import {
7
+ IconAlertTriangle,
8
+ IconArchive,
9
+ IconListDetails,
10
+ IconPlus,
11
+ } from "@tabler/icons-react";
12
+ import { useQueryClient } from "@tanstack/react-query";
13
+ import { useState } from "react";
14
+ import { Link } from "react-router";
15
+ import { toast } from "sonner";
16
+
17
+ import {
18
+ AlertDialog,
19
+ AlertDialogAction,
20
+ AlertDialogCancel,
21
+ AlertDialogContent,
22
+ AlertDialogDescription,
23
+ AlertDialogFooter,
24
+ AlertDialogHeader,
25
+ AlertDialogTitle,
26
+ AlertDialogTrigger,
27
+ } from "@/components/ui/alert-dialog";
28
+ import { Badge } from "@/components/ui/badge";
29
+ import { Button } from "@/components/ui/button";
30
+ import {
31
+ Dialog,
32
+ DialogContent,
33
+ DialogDescription,
34
+ DialogFooter,
35
+ DialogHeader,
36
+ DialogTitle,
37
+ DialogTrigger,
38
+ } from "@/components/ui/dialog";
39
+ import { Input } from "@/components/ui/input";
40
+ import { Label } from "@/components/ui/label";
41
+ import {
42
+ Select,
43
+ SelectContent,
44
+ SelectItem,
45
+ SelectTrigger,
46
+ SelectValue,
47
+ } from "@/components/ui/select";
48
+ import { Textarea } from "@/components/ui/textarea";
49
+
50
+ interface CrmListRow {
51
+ id: string;
52
+ connectionId: string;
53
+ name: string;
54
+ apiSlug: string;
55
+ parentObjectType: string;
56
+ description: string | null;
57
+ archived: boolean;
58
+ entryCount?: number;
59
+ }
60
+
61
+ interface CrmConnectionSummary {
62
+ id: string;
63
+ label: string;
64
+ objectTypes: string[];
65
+ }
66
+
67
+ const LISTS_PARAMS = { limit: 100 } as const;
68
+ const LISTS_KEY = ["action", "list-crm-lists", LISTS_PARAMS];
69
+
70
+ export function ListsSettings() {
71
+ const t = useT();
72
+ const queryClient = useQueryClient();
73
+ const listsQuery = useActionQuery<{ lists: CrmListRow[] }>(
74
+ "list-crm-lists" as never,
75
+ LISTS_PARAMS as never,
76
+ );
77
+ const connectionsQuery = useActionQuery<{
78
+ connections: CrmConnectionSummary[];
79
+ }>("list-crm-connections" as never, {} as never);
80
+ const archive = useActionMutation<
81
+ unknown,
82
+ { listId: string; archived: boolean }
83
+ >("update-crm-list" as never);
84
+ const lists = listsQuery.data?.lists ?? [];
85
+
86
+ async function archiveList(list: CrmListRow) {
87
+ void queryClient.cancelQueries({ queryKey: LISTS_KEY });
88
+ const previous = queryClient.getQueryData<{ lists: CrmListRow[] }>(
89
+ LISTS_KEY,
90
+ );
91
+ queryClient.setQueryData(LISTS_KEY, {
92
+ ...previous,
93
+ lists: (previous?.lists ?? []).filter((row) => row.id !== list.id),
94
+ });
95
+ try {
96
+ await archive.mutateAsync({ listId: list.id, archived: true });
97
+ } catch (error) {
98
+ queryClient.setQueryData(LISTS_KEY, previous);
99
+ toast.error(
100
+ error instanceof Error ? error.message : t("lists.archiveFailed"),
101
+ );
102
+ }
103
+ }
104
+
105
+ return (
106
+ <div className="mx-auto w-full max-w-2xl">
107
+ <div className="flex flex-wrap items-start justify-between gap-4">
108
+ <div className="min-w-0">
109
+ <h1 className="text-xl font-semibold tracking-tight">
110
+ {t("lists.title")}
111
+ </h1>
112
+ <p className="mt-1 max-w-xl text-sm leading-6 text-muted-foreground">
113
+ {t("lists.description")}
114
+ </p>
115
+ </div>
116
+ <CreateListDialog
117
+ connections={connectionsQuery.data?.connections ?? []}
118
+ onCreated={() =>
119
+ void queryClient.invalidateQueries({
120
+ queryKey: ["action", "list-crm-lists"],
121
+ })
122
+ }
123
+ />
124
+ </div>
125
+
126
+ {listsQuery.isError ? (
127
+ <div className="mt-6 flex items-center gap-3 rounded-lg border border-destructive/40 bg-destructive/5 px-4 py-3">
128
+ <IconAlertTriangle className="size-4 shrink-0 text-destructive" />
129
+ <p className="flex-1 text-sm">
130
+ {listsQuery.error instanceof Error
131
+ ? listsQuery.error.message
132
+ : t("lists.loadFailed")}
133
+ </p>
134
+ <Button
135
+ variant="outline"
136
+ size="sm"
137
+ onClick={() => void listsQuery.refetch()}
138
+ >
139
+ {t("lists.retry")}
140
+ </Button>
141
+ </div>
142
+ ) : lists.length ? (
143
+ <div className="mt-6 divide-y divide-border/70 rounded-lg border border-border/70 bg-card">
144
+ {lists.map((list) => (
145
+ <section
146
+ key={list.id}
147
+ className="flex flex-wrap items-center gap-4 px-4 py-3.5"
148
+ >
149
+ <IconListDetails className="size-4 shrink-0 text-muted-foreground" />
150
+ <div className="min-w-0 flex-1">
151
+ <div className="flex flex-wrap items-center gap-2">
152
+ <Link
153
+ to={`/lists/${encodeURIComponent(list.id)}`}
154
+ className="truncate text-sm font-medium hover:underline"
155
+ >
156
+ {list.name}
157
+ </Link>
158
+ <Badge variant="outline" className="font-normal">
159
+ {list.parentObjectType}
160
+ </Badge>
161
+ </div>
162
+ <p className="mt-1 text-xs text-muted-foreground">
163
+ {t("lists.entryCount", { entries: list.entryCount ?? 0 })}
164
+ {" · "}
165
+ <code>{list.apiSlug}</code>
166
+ </p>
167
+ </div>
168
+ <AlertDialog>
169
+ <AlertDialogTrigger asChild>
170
+ <Button
171
+ variant="ghost"
172
+ size="icon"
173
+ aria-label={t("lists.archiveAria", { name: list.name })}
174
+ >
175
+ <IconArchive className="size-4" />
176
+ </Button>
177
+ </AlertDialogTrigger>
178
+ <AlertDialogContent>
179
+ <AlertDialogHeader>
180
+ <AlertDialogTitle>
181
+ {t("lists.archiveTitle", { name: list.name })}
182
+ </AlertDialogTitle>
183
+ <AlertDialogDescription>
184
+ {t("lists.archiveDescription")}
185
+ </AlertDialogDescription>
186
+ </AlertDialogHeader>
187
+ <AlertDialogFooter>
188
+ <AlertDialogCancel>{t("lists.cancel")}</AlertDialogCancel>
189
+ <AlertDialogAction onClick={() => void archiveList(list)}>
190
+ {t("lists.archive")}
191
+ </AlertDialogAction>
192
+ </AlertDialogFooter>
193
+ </AlertDialogContent>
194
+ </AlertDialog>
195
+ </section>
196
+ ))}
197
+ </div>
198
+ ) : (
199
+ <div className="mt-6 rounded-lg border border-dashed border-border px-4 py-10 text-center">
200
+ <p className="text-sm font-medium">
201
+ {listsQuery.isLoading ? t("lists.loading") : t("lists.emptyTitle")}
202
+ </p>
203
+ <p className="mt-1 text-sm text-muted-foreground">
204
+ {t("lists.emptyDescription")}
205
+ </p>
206
+ </div>
207
+ )}
208
+ </div>
209
+ );
210
+ }
211
+
212
+ function CreateListDialog({
213
+ connections,
214
+ onCreated,
215
+ }: {
216
+ connections: CrmConnectionSummary[];
217
+ onCreated: () => void;
218
+ }) {
219
+ const t = useT();
220
+ const [open, setOpen] = useState(false);
221
+ const [name, setName] = useState("");
222
+ const [description, setDescription] = useState("");
223
+ const [parentObjectType, setParentObjectType] = useState("");
224
+ const create = useActionMutation<
225
+ unknown,
226
+ {
227
+ name: string;
228
+ parentObjectType: string;
229
+ description?: string;
230
+ connectionId?: string;
231
+ }
232
+ >("create-crm-list" as never);
233
+
234
+ const objectTypes = connections.flatMap((connection) =>
235
+ connection.objectTypes.map((objectType) => ({
236
+ connectionId: connection.id,
237
+ objectType,
238
+ label: `${connection.label} · ${objectType}`,
239
+ key: `${connection.id}:${objectType}`,
240
+ })),
241
+ );
242
+ const selected = objectTypes.find((entry) => entry.key === parentObjectType);
243
+
244
+ function openChange(next: boolean) {
245
+ if (!next) {
246
+ setName("");
247
+ setDescription("");
248
+ setParentObjectType("");
249
+ }
250
+ setOpen(next);
251
+ }
252
+
253
+ async function submit() {
254
+ if (!selected) return;
255
+ try {
256
+ await create.mutateAsync({
257
+ name: name.trim(),
258
+ parentObjectType: selected.objectType,
259
+ connectionId: selected.connectionId,
260
+ ...(description.trim() ? { description: description.trim() } : {}),
261
+ });
262
+ openChange(false);
263
+ onCreated();
264
+ toast.success(t("lists.created"));
265
+ } catch (error) {
266
+ toast.error(
267
+ error instanceof Error ? error.message : t("lists.createFailed"),
268
+ );
269
+ }
270
+ }
271
+
272
+ return (
273
+ <Dialog open={open} onOpenChange={openChange}>
274
+ <DialogTrigger asChild>
275
+ <Button size="sm" className="gap-1.5">
276
+ <IconPlus className="size-4" /> {t("lists.newList")}
277
+ </Button>
278
+ </DialogTrigger>
279
+ <DialogContent className="sm:max-w-lg">
280
+ <DialogHeader>
281
+ <DialogTitle>{t("lists.createTitle")}</DialogTitle>
282
+ <DialogDescription>{t("lists.createDescription")}</DialogDescription>
283
+ </DialogHeader>
284
+ <div className="grid gap-4 py-2">
285
+ <div className="grid gap-2">
286
+ <Label htmlFor="new-list-name">{t("lists.name")}</Label>
287
+ <Input
288
+ id="new-list-name"
289
+ value={name}
290
+ maxLength={120}
291
+ onChange={(event) => setName(event.target.value)}
292
+ />
293
+ </div>
294
+ <div className="grid gap-2">
295
+ <Label htmlFor="new-list-object">{t("lists.parentObject")}</Label>
296
+ <Select
297
+ value={parentObjectType}
298
+ onValueChange={setParentObjectType}
299
+ disabled={!objectTypes.length}
300
+ >
301
+ <SelectTrigger id="new-list-object">
302
+ <SelectValue placeholder={t("lists.parentObjectPlaceholder")} />
303
+ </SelectTrigger>
304
+ <SelectContent>
305
+ {objectTypes.map((entry) => (
306
+ <SelectItem key={entry.key} value={entry.key}>
307
+ {entry.label}
308
+ </SelectItem>
309
+ ))}
310
+ </SelectContent>
311
+ </Select>
312
+ </div>
313
+ <div className="grid gap-2">
314
+ <Label htmlFor="new-list-description">
315
+ {t("lists.listDescription")}
316
+ </Label>
317
+ <Textarea
318
+ id="new-list-description"
319
+ value={description}
320
+ maxLength={500}
321
+ onChange={(event) => setDescription(event.target.value)}
322
+ />
323
+ </div>
324
+ <p className="text-xs leading-5 text-muted-foreground">
325
+ {t("lists.stageSeedNote")}
326
+ </p>
327
+ </div>
328
+ <DialogFooter>
329
+ <Button
330
+ disabled={!name.trim() || !selected || create.isPending}
331
+ onClick={() => void submit()}
332
+ >
333
+ {t("lists.create")}
334
+ </Button>
335
+ </DialogFooter>
336
+ </DialogContent>
337
+ </Dialog>
338
+ );
339
+ }
@@ -0,0 +1,377 @@
1
+ /**
2
+ * Decision logic behind the CRM settings surfaces.
3
+ *
4
+ * It lives outside the components because `vitest.config.ts` collects
5
+ * `**\/*.test.ts` in a node environment: a `.tsx` component cannot be reached
6
+ * from a test at all, so anything worth proving has to be a plain module.
7
+ *
8
+ * Type behaviour is read from `ATTRIBUTE_TYPE_SPECS`, never re-derived here.
9
+ */
10
+
11
+ import {
12
+ ATTRIBUTE_TYPE_SPECS,
13
+ CRM_ATTRIBUTE_TYPES,
14
+ type CrmAttributeType,
15
+ } from "../../../../shared/crm-attributes";
16
+ import {
17
+ CRM_CONNECTION_MODES,
18
+ type CrmAttributeAuthority,
19
+ type CrmAttributeDefinition,
20
+ type CrmAttributeOption,
21
+ type CrmConnectionMode,
22
+ } from "../../../../shared/crm-contract";
23
+
24
+ // ---------------------------------------------------------------------------
25
+ // Attribute types
26
+ // ---------------------------------------------------------------------------
27
+
28
+ /** Types a person may author. `interaction` and `personal-name` are system-only. */
29
+ export const AUTHORED_ATTRIBUTE_TYPES: readonly CrmAttributeType[] =
30
+ CRM_ATTRIBUTE_TYPES.filter((type) => !ATTRIBUTE_TYPE_SPECS[type].systemOnly);
31
+
32
+ export interface AttributeTypeCapabilities {
33
+ supportsMulti: boolean;
34
+ usesOptions: boolean;
35
+ /** `targetDays` and `celebrate` describe a pipeline stage: status only. */
36
+ showsStageFields: boolean;
37
+ }
38
+
39
+ export function attributeTypeCapabilities(
40
+ type: CrmAttributeType,
41
+ ): AttributeTypeCapabilities {
42
+ const spec = ATTRIBUTE_TYPE_SPECS[type];
43
+ return {
44
+ supportsMulti: spec.supportsMulti,
45
+ usesOptions: spec.usesOptions,
46
+ showsStageFields: type === "status",
47
+ };
48
+ }
49
+
50
+ /**
51
+ * `api_slug` keys every stored value row and the type chose the column those
52
+ * values live in, so `update-crm-attribute` rejects a change to either.
53
+ * Rendering them as editable would offer a control the server always refuses.
54
+ */
55
+ export const IMMUTABLE_ATTRIBUTE_FIELDS = ["apiSlug", "attributeType"] as const;
56
+
57
+ export type ImmutableAttributeField =
58
+ (typeof IMMUTABLE_ATTRIBUTE_FIELDS)[number];
59
+
60
+ export function isImmutableAttributeField(
61
+ field: string,
62
+ ): field is ImmutableAttributeField {
63
+ return (IMMUTABLE_ATTRIBUTE_FIELDS as readonly string[]).includes(field);
64
+ }
65
+
66
+ // ---------------------------------------------------------------------------
67
+ // Authority
68
+ // ---------------------------------------------------------------------------
69
+
70
+ export interface AttributeAuthorityInfo {
71
+ labelKey: string;
72
+ descriptionKey: string;
73
+ /** A local edit cannot be written upstream; it is recorded as a proposal. */
74
+ editsBecomeProposals: boolean;
75
+ badge: "default" | "secondary" | "outline";
76
+ }
77
+
78
+ export const ATTRIBUTE_AUTHORITY_INFO: Record<
79
+ CrmAttributeAuthority,
80
+ AttributeAuthorityInfo
81
+ > = {
82
+ provider: {
83
+ labelKey: "fields.authorityProvider",
84
+ descriptionKey: "fields.authorityProviderHelp",
85
+ editsBecomeProposals: true,
86
+ badge: "secondary",
87
+ },
88
+ "derived-local": {
89
+ labelKey: "fields.authorityDerived",
90
+ descriptionKey: "fields.authorityDerivedHelp",
91
+ editsBecomeProposals: false,
92
+ badge: "outline",
93
+ },
94
+ "local-authoritative": {
95
+ labelKey: "fields.authorityLocal",
96
+ descriptionKey: "fields.authorityLocalHelp",
97
+ editsBecomeProposals: false,
98
+ badge: "default",
99
+ },
100
+ };
101
+
102
+ // ---------------------------------------------------------------------------
103
+ // Drafts and action payloads
104
+ // ---------------------------------------------------------------------------
105
+
106
+ export interface AttributeOptionDraft {
107
+ value: string;
108
+ title: string;
109
+ color: string | null;
110
+ targetDays: number | null;
111
+ celebrate: boolean;
112
+ }
113
+
114
+ export interface AttributeDraft {
115
+ title: string;
116
+ type: CrmAttributeType;
117
+ description: string;
118
+ multi: boolean;
119
+ required: boolean;
120
+ uniqueValue: boolean;
121
+ historyTracked: boolean;
122
+ options: AttributeOptionDraft[];
123
+ }
124
+
125
+ export function emptyAttributeDraft(): AttributeDraft {
126
+ return {
127
+ title: "",
128
+ type: "text",
129
+ description: "",
130
+ multi: false,
131
+ required: false,
132
+ uniqueValue: false,
133
+ historyTracked: true,
134
+ options: [],
135
+ };
136
+ }
137
+
138
+ export function emptyAttributeOptionDraft(): AttributeOptionDraft {
139
+ return {
140
+ value: "",
141
+ title: "",
142
+ color: null,
143
+ targetDays: null,
144
+ celebrate: false,
145
+ };
146
+ }
147
+
148
+ export interface CrmAttributeTarget {
149
+ target: "object" | "list";
150
+ targetId: string;
151
+ connectionId?: string;
152
+ }
153
+
154
+ export interface CreateAttributeInput extends CrmAttributeTarget {
155
+ title: string;
156
+ type: CrmAttributeType;
157
+ multi: boolean;
158
+ required: boolean;
159
+ uniqueValue: boolean;
160
+ historyTracked: boolean;
161
+ description?: string;
162
+ options?: Array<{
163
+ value: string;
164
+ title: string;
165
+ color?: string;
166
+ targetDays?: number | null;
167
+ celebrate?: boolean;
168
+ }>;
169
+ }
170
+
171
+ /**
172
+ * The exact payload `create-crm-attribute` accepts for a draft. Options and
173
+ * stage fields are dropped for types that do not take them: the action rejects
174
+ * both with a 422, and the picker that produced the draft may have been showing
175
+ * them before the type changed.
176
+ */
177
+ export function buildCreateAttributeInput(
178
+ draft: AttributeDraft,
179
+ target: CrmAttributeTarget,
180
+ ): CreateAttributeInput {
181
+ const capabilities = attributeTypeCapabilities(draft.type);
182
+ const description = draft.description.trim();
183
+ const options = capabilities.usesOptions
184
+ ? draft.options
185
+ .filter((option) => option.value.trim())
186
+ .map((option) => ({
187
+ value: option.value.trim(),
188
+ title: option.title.trim() || option.value.trim(),
189
+ ...(option.color ? { color: option.color } : {}),
190
+ ...(capabilities.showsStageFields
191
+ ? { targetDays: option.targetDays, celebrate: option.celebrate }
192
+ : {}),
193
+ }))
194
+ : [];
195
+
196
+ return {
197
+ target: target.target,
198
+ targetId: target.targetId,
199
+ ...(target.connectionId ? { connectionId: target.connectionId } : {}),
200
+ title: draft.title.trim(),
201
+ type: draft.type,
202
+ multi: capabilities.supportsMulti && draft.multi,
203
+ required: draft.required,
204
+ uniqueValue: draft.uniqueValue,
205
+ historyTracked: draft.historyTracked,
206
+ ...(description ? { description } : {}),
207
+ ...(options.length ? { options } : {}),
208
+ };
209
+ }
210
+
211
+ export interface AttributeEditDraft {
212
+ title: string;
213
+ description: string;
214
+ required: boolean;
215
+ historyTracked: boolean;
216
+ }
217
+
218
+ export interface UpdateAttributeInput {
219
+ attributeId: string;
220
+ title?: string;
221
+ description?: string | null;
222
+ required?: boolean;
223
+ historyTracked?: boolean;
224
+ }
225
+
226
+ export function attributeEditDraft(
227
+ attribute: CrmAttributeDefinition,
228
+ ): AttributeEditDraft {
229
+ return {
230
+ title: attribute.label,
231
+ description: attribute.description ?? "",
232
+ required: attribute.required,
233
+ historyTracked: attribute.historyTracked,
234
+ };
235
+ }
236
+
237
+ /**
238
+ * Only the fields that actually changed, and never `apiSlug` or `type`: the
239
+ * action accepts those two solely to reject them with an explanation.
240
+ */
241
+ export function buildUpdateAttributeInput(
242
+ attribute: CrmAttributeDefinition,
243
+ draft: AttributeEditDraft,
244
+ ): UpdateAttributeInput {
245
+ const title = draft.title.trim();
246
+ const description = draft.description.trim();
247
+ const current = attributeEditDraft(attribute);
248
+ return {
249
+ attributeId: attribute.id,
250
+ ...(title && title !== current.title ? { title } : {}),
251
+ ...(description === current.description.trim()
252
+ ? {}
253
+ : { description: description || null }),
254
+ ...(draft.required === current.required
255
+ ? {}
256
+ : { required: draft.required }),
257
+ ...(draft.historyTracked === current.historyTracked
258
+ ? {}
259
+ : { historyTracked: draft.historyTracked }),
260
+ };
261
+ }
262
+
263
+ export function hasAttributeEdits(input: UpdateAttributeInput): boolean {
264
+ return Object.keys(input).length > 1;
265
+ }
266
+
267
+ // ---------------------------------------------------------------------------
268
+ // Optimistic cache patching
269
+ // ---------------------------------------------------------------------------
270
+
271
+ export interface CrmAttributeListResult {
272
+ target: "object" | "list";
273
+ targetId: string;
274
+ attributes: CrmAttributeDefinition[];
275
+ }
276
+
277
+ export function applyAttributePatch(
278
+ result: CrmAttributeListResult | undefined,
279
+ attributeId: string,
280
+ patch: Partial<CrmAttributeDefinition>,
281
+ ): CrmAttributeListResult | undefined {
282
+ if (!result) return result;
283
+ return {
284
+ ...result,
285
+ attributes: result.attributes.map((attribute) =>
286
+ attribute.id === attributeId ? { ...attribute, ...patch } : attribute,
287
+ ),
288
+ };
289
+ }
290
+
291
+ // ---------------------------------------------------------------------------
292
+ // Option ordering
293
+ // ---------------------------------------------------------------------------
294
+
295
+ /** A copy with `from` moved to `to`. Out-of-range indices leave the order alone. */
296
+ export function moveItem<T>(
297
+ items: readonly T[],
298
+ from: number,
299
+ to: number,
300
+ ): T[] {
301
+ const next = [...items];
302
+ if (
303
+ from < 0 ||
304
+ to < 0 ||
305
+ from >= next.length ||
306
+ to >= next.length ||
307
+ from === to
308
+ ) {
309
+ return next;
310
+ }
311
+ const [moved] = next.splice(from, 1);
312
+ next.splice(to, 0, moved as T);
313
+ return next;
314
+ }
315
+
316
+ /** The `optionIds` payload `manage-crm-attribute-option` reorder expects. */
317
+ export function reorderedOptionIds(
318
+ options: readonly CrmAttributeOption[],
319
+ from: number,
320
+ to: number,
321
+ ): string[] {
322
+ return moveItem(options, from, to).map((option) => option.id);
323
+ }
324
+
325
+ // ---------------------------------------------------------------------------
326
+ // Connection modes
327
+ // ---------------------------------------------------------------------------
328
+
329
+ export interface CrmConnectionModeInfo {
330
+ mode: CrmConnectionMode;
331
+ labelKey: string;
332
+ descriptionKey: string;
333
+ /** Deprecated modes still render for existing rows; they are never offered. */
334
+ deprecated: boolean;
335
+ }
336
+
337
+ export const CRM_CONNECTION_MODE_INFO: Record<
338
+ CrmConnectionMode,
339
+ CrmConnectionModeInfo
340
+ > = {
341
+ native: {
342
+ mode: "native",
343
+ labelKey: "connection.modeNative",
344
+ descriptionKey: "connection.modeNativeHelp",
345
+ deprecated: false,
346
+ },
347
+ connected: {
348
+ mode: "connected",
349
+ labelKey: "connection.modeConnected",
350
+ descriptionKey: "connection.modeConnectedHelp",
351
+ deprecated: false,
352
+ },
353
+ hybrid: {
354
+ mode: "hybrid",
355
+ labelKey: "connection.modeHybrid",
356
+ descriptionKey: "connection.modeHybridHelp",
357
+ deprecated: true,
358
+ },
359
+ };
360
+
361
+ /**
362
+ * The only set a mode picker may offer. `hybrid` is derived out rather than
363
+ * hand-omitted so it cannot reappear by someone extending a literal list.
364
+ */
365
+ export const SELECTABLE_CRM_CONNECTION_MODES: readonly CrmConnectionMode[] =
366
+ CRM_CONNECTION_MODES.filter(
367
+ (mode) => !CRM_CONNECTION_MODE_INFO[mode].deprecated,
368
+ );
369
+
370
+ /**
371
+ * Null for a mode this build does not know. The caller must render that as
372
+ * unrecognized — falling back to a default would print a confident wrong label
373
+ * for a row whose real mode nobody can read.
374
+ */
375
+ export function connectionModeInfo(mode: string): CrmConnectionModeInfo | null {
376
+ return CRM_CONNECTION_MODE_INFO[mode as CrmConnectionMode] ?? null;
377
+ }