@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,296 @@
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
+ CrmFilterError,
9
+ crmFilterSchema,
10
+ crmSortSchema,
11
+ hydrateSavedViewRow,
12
+ normalizeStoredColumns,
13
+ normalizeStoredFilter,
14
+ } from "../server/lib/crm-query.js";
15
+ import { requireCrmScope, toJson } from "./_crm-action-utils.js";
16
+
17
+ /** A save that lost a race — surfaces as HTTP 409 instead of clobbering. */
18
+ class CrmSavedViewConflictError extends Error {
19
+ readonly statusCode = 409;
20
+ readonly code = "crm-saved-view-conflict";
21
+
22
+ constructor(id: string, expected: string, actual: string) {
23
+ super(
24
+ `CRM saved view "${id}" changed between read and write (expected ${expected}, found ${actual}). Re-read it and re-apply your edit.`,
25
+ );
26
+ this.name = "CrmSavedViewConflictError";
27
+ }
28
+ }
29
+
30
+ const columnsSchema = z
31
+ .array(
32
+ z.union([
33
+ z.string().trim().min(1).max(128),
34
+ z.object({
35
+ attributeId: z.string().trim().min(1).max(128),
36
+ width: z.number().int().min(40).max(1_200).optional(),
37
+ hidden: z.boolean().optional(),
38
+ }),
39
+ ]),
40
+ )
41
+ .max(60);
42
+
43
+ export default defineAction({
44
+ description:
45
+ "Create or update a bounded, access-scoped CRM saved view: its typed filter, ordered sort, columns, table/board presentation, and personal/shared audience. Pass expectedUpdatedAt to reject an overwrite when the view changed since it was read. A saved view stores filters and presentation settings, never provider rows.",
46
+ schema: z.object({
47
+ id: z.string().min(1).max(128).optional(),
48
+ name: z.string().trim().min(1).max(120),
49
+ description: z.string().trim().max(500).optional(),
50
+ /** Record kind (account/person/opportunity) — not the table/board kind. */
51
+ kind: z.enum(["account", "person", "opportunity"]).optional(),
52
+ viewKind: z.enum(["table", "board"]).optional(),
53
+ targetKind: z.enum(["object", "list"]).optional(),
54
+ targetId: z.string().trim().min(1).max(128).optional(),
55
+ groupByAttributeId: z
56
+ .string()
57
+ .trim()
58
+ .min(1)
59
+ .max(128)
60
+ .optional()
61
+ .describe("Board views only; must reference a status attribute."),
62
+ filter: crmFilterSchema.optional(),
63
+ /** Legacy `{query, fieldEquals}` blob; normalized into `filter`. */
64
+ filters: z.record(z.string().max(120), z.unknown()).optional(),
65
+ columns: columnsSchema.optional(),
66
+ sort: crmSortSchema.optional(),
67
+ dataProgramId: z.string().trim().min(1).max(128).optional(),
68
+ pinned: z.boolean().optional(),
69
+ audience: z.enum(["personal", "shared"]).optional(),
70
+ expectedUpdatedAt: z.string().min(1).max(64).optional(),
71
+ }),
72
+ audit: {
73
+ target: (_args, result) => {
74
+ const view = result as {
75
+ id: string;
76
+ ownerEmail: string;
77
+ orgId: string | null;
78
+ visibility: "private" | "org";
79
+ };
80
+ return {
81
+ type: "crm-saved-view",
82
+ id: view.id,
83
+ ownerEmail: view.ownerEmail,
84
+ orgId: view.orgId,
85
+ visibility: view.visibility,
86
+ };
87
+ },
88
+ summary: (args) => `Saved CRM view ${args.name}`,
89
+ },
90
+ run: async (args, ctx?: ActionRunContext) => {
91
+ if (args.filter && args.filters) {
92
+ throw new CrmFilterError(
93
+ "crm-filter-conflict",
94
+ "Pass either filter or the legacy filters blob, not both.",
95
+ );
96
+ }
97
+ const db = getDb();
98
+ const now = new Date().toISOString();
99
+ const scope = requireCrmScope(ctx);
100
+
101
+ const filter =
102
+ args.filter ??
103
+ (args.filters === undefined
104
+ ? undefined
105
+ : normalizeStoredFilter(args.filters));
106
+ const columns =
107
+ args.columns === undefined
108
+ ? undefined
109
+ : normalizeStoredColumns(args.columns);
110
+
111
+ const existing = args.id ? await readView(args.id) : undefined;
112
+ if (args.id && !existing) throw new Error("CRM saved view was not found.");
113
+
114
+ const viewKind = args.viewKind ?? existing?.viewKind ?? "table";
115
+ // Switching a board back to a table drops its grouping instead of
116
+ // inheriting it — otherwise the switch would fail the board-only check.
117
+ const groupByAttributeId =
118
+ args.groupByAttributeId !== undefined
119
+ ? args.groupByAttributeId
120
+ : viewKind === "board"
121
+ ? (existing?.groupByAttributeId ?? null)
122
+ : null;
123
+ if (viewKind === "board") {
124
+ if (!groupByAttributeId) {
125
+ throw new CrmFilterError(
126
+ "crm-saved-view-group-by",
127
+ "A board view needs groupByAttributeId naming a status attribute.",
128
+ );
129
+ }
130
+ await assertStatusAttribute(groupByAttributeId);
131
+ } else if (groupByAttributeId) {
132
+ throw new CrmFilterError(
133
+ "crm-saved-view-group-by",
134
+ "groupByAttributeId only applies to a board view.",
135
+ );
136
+ }
137
+
138
+ const visibility =
139
+ args.audience === undefined
140
+ ? undefined
141
+ : audienceVisibility(args.audience, scope.orgId);
142
+
143
+ if (args.id && existing) {
144
+ await assertAccess("crm-saved-view", args.id, "editor");
145
+ if (
146
+ args.expectedUpdatedAt !== undefined &&
147
+ args.expectedUpdatedAt !== existing.updatedAt
148
+ ) {
149
+ throw new CrmSavedViewConflictError(
150
+ args.id,
151
+ args.expectedUpdatedAt,
152
+ existing.updatedAt,
153
+ );
154
+ }
155
+ await db
156
+ .update(schema.crmSavedViews)
157
+ .set({
158
+ name: args.name,
159
+ ...(args.description !== undefined
160
+ ? { description: args.description }
161
+ : {}),
162
+ ...(args.kind !== undefined ? { kind: args.kind } : {}),
163
+ ...(args.viewKind !== undefined ? { viewKind } : {}),
164
+ ...(args.targetKind !== undefined
165
+ ? { targetKind: args.targetKind }
166
+ : {}),
167
+ ...(args.targetId !== undefined ? { targetId: args.targetId } : {}),
168
+ groupByAttributeId,
169
+ ...(filter !== undefined
170
+ ? { filtersJson: toJson(filter, 8_000) }
171
+ : {}),
172
+ ...(columns !== undefined
173
+ ? { columnsJson: toJson(columns, 8_000) }
174
+ : {}),
175
+ ...(args.sort !== undefined
176
+ ? { sortJson: toJson(args.sort, 2_000) }
177
+ : {}),
178
+ ...(args.dataProgramId !== undefined
179
+ ? { dataProgramId: args.dataProgramId }
180
+ : {}),
181
+ ...(args.pinned !== undefined ? { pinned: args.pinned } : {}),
182
+ ...(visibility !== undefined ? { visibility } : {}),
183
+ updatedAt: now,
184
+ })
185
+ .where(
186
+ and(
187
+ eq(schema.crmSavedViews.id, args.id),
188
+ ...(args.expectedUpdatedAt !== undefined
189
+ ? [eq(schema.crmSavedViews.updatedAt, args.expectedUpdatedAt)]
190
+ : []),
191
+ ),
192
+ );
193
+ const saved = await readView(args.id);
194
+ if (!saved) throw new Error("CRM saved view was not found.");
195
+ if (args.expectedUpdatedAt !== undefined && saved.updatedAt !== now) {
196
+ // The guarded UPDATE matched nothing: another writer landed between the
197
+ // read above and the write. Report it rather than returning the row we
198
+ // did not write as if the save succeeded.
199
+ throw new CrmSavedViewConflictError(
200
+ args.id,
201
+ args.expectedUpdatedAt,
202
+ saved.updatedAt,
203
+ );
204
+ }
205
+ return saved;
206
+ }
207
+
208
+ const id = crypto.randomUUID();
209
+ await db.insert(schema.crmSavedViews).values({
210
+ id,
211
+ name: args.name,
212
+ description: args.description ?? "",
213
+ kind: args.kind ?? null,
214
+ viewKind,
215
+ targetKind: args.targetKind ?? "object",
216
+ targetId: args.targetId ?? null,
217
+ groupByAttributeId,
218
+ filtersJson: toJson(filter ?? { op: "and", conditions: [] }, 8_000),
219
+ columnsJson: toJson(columns ?? [], 8_000),
220
+ sortJson: toJson(args.sort ?? [], 2_000),
221
+ dataProgramId: args.dataProgramId ?? null,
222
+ pinned: args.pinned ?? false,
223
+ ...scope,
224
+ ...(visibility !== undefined ? { visibility } : {}),
225
+ createdAt: now,
226
+ updatedAt: now,
227
+ });
228
+ const saved = await readView(id);
229
+ if (!saved)
230
+ throw new Error("CRM saved view could not be verified after saving.");
231
+ return saved;
232
+ },
233
+ });
234
+
235
+ function audienceVisibility(
236
+ audience: "personal" | "shared",
237
+ orgId: string | null,
238
+ ): "private" | "org" {
239
+ if (audience === "personal") return "private";
240
+ if (!orgId) {
241
+ throw new CrmFilterError(
242
+ "crm-saved-view-audience",
243
+ "A shared view needs an active organization; this session has none.",
244
+ );
245
+ }
246
+ return "org";
247
+ }
248
+
249
+ async function assertStatusAttribute(attributeId: string): Promise<void> {
250
+ const [row] = await getDb()
251
+ .select({
252
+ attributeType: schema.crmFieldPolicies.attributeType,
253
+ archived: schema.crmFieldPolicies.archived,
254
+ })
255
+ .from(schema.crmFieldPolicies)
256
+ .where(
257
+ and(
258
+ eq(schema.crmFieldPolicies.id, attributeId),
259
+ accessFilter(schema.crmFieldPolicies, schema.crmFieldPolicyShares),
260
+ ),
261
+ )
262
+ .limit(1);
263
+ if (!row) {
264
+ throw new CrmFilterError(
265
+ "crm-saved-view-group-by",
266
+ `Board grouping references unknown attribute "${attributeId}".`,
267
+ );
268
+ }
269
+ if (row.archived) {
270
+ throw new CrmFilterError(
271
+ "crm-saved-view-group-by",
272
+ `Board grouping references archived attribute "${attributeId}".`,
273
+ );
274
+ }
275
+ if (row.attributeType !== "status") {
276
+ throw new CrmFilterError(
277
+ "crm-saved-view-group-by",
278
+ `Board grouping needs a status attribute; "${attributeId}" is ${row.attributeType}.`,
279
+ );
280
+ }
281
+ }
282
+
283
+ async function readView(id: string) {
284
+ const [row] = await getDb()
285
+ .select()
286
+ .from(schema.crmSavedViews)
287
+ .where(
288
+ and(
289
+ eq(schema.crmSavedViews.id, id),
290
+ accessFilter(schema.crmSavedViews, schema.crmSavedViewShares),
291
+ ),
292
+ )
293
+ .limit(1);
294
+ if (!row) return undefined;
295
+ return { ...row, view: hydrateSavedViewRow(row) };
296
+ }
@@ -0,0 +1,146 @@
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 {
7
+ createConnectedCrmAdapter,
8
+ isConnectedCrmProvider,
9
+ } from "../server/crm/adapter.js";
10
+ import {
11
+ MAX_SYNC_PAGE_SIZE,
12
+ MAX_SYNC_PAGES,
13
+ syncCrmMirror,
14
+ } from "../server/crm/crm-mirror.js";
15
+ import { getDb, schema } from "../server/db/index.js";
16
+ import { requireCrmScope } from "./_crm-action-utils.js";
17
+
18
+ const idList = z
19
+ .array(z.string().trim().min(1).max(160))
20
+ .min(1)
21
+ .max(50)
22
+ .optional();
23
+ const syncScopeSchema = z
24
+ .object({
25
+ pipelineIds: idList,
26
+ ownerIds: idList,
27
+ recordIds: idList,
28
+ associatedRecordIds: idList,
29
+ updatedAfter: z.string().datetime({ offset: true }).optional(),
30
+ includeDeleted: z.boolean().default(false),
31
+ })
32
+ .superRefine((scope, issue) => {
33
+ if (
34
+ !scope.pipelineIds &&
35
+ !scope.ownerIds &&
36
+ !scope.recordIds &&
37
+ !scope.associatedRecordIds &&
38
+ !scope.updatedAfter
39
+ ) {
40
+ issue.addIssue({
41
+ code: "custom",
42
+ message:
43
+ "A sync requires a bounded cohort selector: pipelineIds, ownerIds, recordIds, associatedRecordIds, or updatedAfter.",
44
+ });
45
+ }
46
+ });
47
+
48
+ export default defineAction({
49
+ description:
50
+ "Thinly mirror one bounded HubSpot or Salesforce CRM object cohort. It discovers field policy first, mirrors only explicit safe fields, and returns a continuation cursor instead of claiming an exhaustive sync.",
51
+ schema: z.object({
52
+ connectionId: z.string().trim().min(1).max(128),
53
+ objectType: z.string().trim().min(1).max(120),
54
+ scope: syncScopeSchema,
55
+ fieldAllowList: z
56
+ .array(z.string().trim().min(1).max(120))
57
+ .min(1)
58
+ .max(80)
59
+ .optional(),
60
+ allowCustomObject: z.boolean().default(false),
61
+ cursor: z.string().trim().min(1).max(2_000).optional(),
62
+ pageSize: z.coerce
63
+ .number()
64
+ .int()
65
+ .min(1)
66
+ .max(MAX_SYNC_PAGE_SIZE)
67
+ .default(MAX_SYNC_PAGE_SIZE),
68
+ maxPages: z.coerce
69
+ .number()
70
+ .int()
71
+ .min(1)
72
+ .max(MAX_SYNC_PAGES)
73
+ .default(MAX_SYNC_PAGES),
74
+ }),
75
+ audit: {
76
+ target: (args, result) => {
77
+ const response = result as {
78
+ ownerEmail: string;
79
+ orgId: string | null;
80
+ visibility: "private" | "org";
81
+ };
82
+ return {
83
+ type: "crm-connection",
84
+ id: args.connectionId,
85
+ ownerEmail: response.ownerEmail,
86
+ orgId: response.orgId,
87
+ visibility: response.visibility,
88
+ };
89
+ },
90
+ summary: (args) => `Synced bounded CRM ${args.objectType} cohort`,
91
+ recordInputs: false,
92
+ },
93
+ run: async (args, ctx?: ActionRunContext) => {
94
+ await assertAccess("crm-connection", args.connectionId, "editor");
95
+ const db = getDb();
96
+ const [connection] = await db
97
+ .select()
98
+ .from(schema.crmConnections)
99
+ .where(
100
+ and(
101
+ eq(schema.crmConnections.id, args.connectionId),
102
+ accessFilter(
103
+ schema.crmConnections,
104
+ schema.crmConnectionShares,
105
+ undefined,
106
+ "editor",
107
+ ),
108
+ ),
109
+ )
110
+ .limit(1);
111
+ if (!connection || connection.status === "disconnected")
112
+ throw new Error("CRM connection is unavailable.");
113
+ if (
114
+ !isConnectedCrmProvider(connection.provider) ||
115
+ !connection.workspaceConnectionId
116
+ )
117
+ throw new Error(
118
+ "CRM sync requires a connected HubSpot or Salesforce workspace connection.",
119
+ );
120
+ const ownership = requireCrmScope(ctx);
121
+ const adapter = await createConnectedCrmAdapter({
122
+ provider: connection.provider,
123
+ connectionId: connection.workspaceConnectionId,
124
+ userEmail: ownership.ownerEmail,
125
+ orgId: ownership.orgId,
126
+ });
127
+ const result = await syncCrmMirror({
128
+ connectionId: connection.id,
129
+ objectType: args.objectType,
130
+ scope: { objectType: args.objectType, ...args.scope },
131
+ fieldAllowList: args.fieldAllowList,
132
+ allowCustomObject: args.allowCustomObject,
133
+ cursor: args.cursor,
134
+ pageSize: args.pageSize,
135
+ maxPages: args.maxPages,
136
+ ownership,
137
+ adapter,
138
+ });
139
+ return {
140
+ ...result,
141
+ ownerEmail: ownership.ownerEmail,
142
+ orgId: ownership.orgId,
143
+ visibility: ownership.visibility,
144
+ };
145
+ },
146
+ });
@@ -0,0 +1,111 @@
1
+ import { defineAction } from "@agent-native/core/action";
2
+ import { eq } from "drizzle-orm";
3
+ import { z } from "zod";
4
+
5
+ import { getDb, schema } from "../server/db/index.js";
6
+ import {
7
+ CRM_ATTRIBUTE_FILL_MODES,
8
+ CRM_ATTRIBUTE_TYPES,
9
+ } from "../shared/crm-contract.js";
10
+ import { toJson } from "./_crm-action-utils.js";
11
+ import {
12
+ CrmAttributeValueError,
13
+ loadAttributeOptions,
14
+ requireEditableAttribute,
15
+ toAttributeDefinition,
16
+ } from "./_crm-attribute-utils.js";
17
+
18
+ const MAX_CONFIG_CHARS = 4000;
19
+
20
+ export default defineAction({
21
+ description:
22
+ "Update one CRM attribute's presentation and behaviour: title, description, position, required, historyTracked, config, archived, and the manual fill mode. The api slug and the attribute type are immutable — every stored value row is keyed by the slug and typed by the type, so changing either would orphan history instead of migrating it. Setting fillMode never runs a fill; fills are triggered manually.",
23
+ schema: z.object({
24
+ attributeId: z.string().trim().min(1).max(128),
25
+ title: z.string().trim().min(1).max(200).optional(),
26
+ description: z.string().trim().max(2000).nullish(),
27
+ position: z.number().int().min(0).max(10_000).optional(),
28
+ required: z.boolean().optional(),
29
+ historyTracked: z.boolean().optional(),
30
+ config: z.record(z.string(), z.unknown()).optional(),
31
+ fillMode: z.enum(CRM_ATTRIBUTE_FILL_MODES).nullish(),
32
+ fillConfig: z.record(z.string(), z.unknown()).optional(),
33
+ archived: z.boolean().optional(),
34
+ apiSlug: z
35
+ .string()
36
+ .trim()
37
+ .min(1)
38
+ .max(128)
39
+ .optional()
40
+ .describe("Immutable. Supplying a different slug is rejected."),
41
+ type: z
42
+ .enum(CRM_ATTRIBUTE_TYPES)
43
+ .optional()
44
+ .describe("Immutable. Supplying a different type is rejected."),
45
+ }),
46
+ audit: {
47
+ target: (args) => ({ type: "crm-field-policy", id: args.attributeId }),
48
+ summary: (args) => `Updated CRM attribute ${args.attributeId}`,
49
+ },
50
+ run: async (args) => {
51
+ const current = await requireEditableAttribute(args.attributeId);
52
+
53
+ if (
54
+ args.apiSlug &&
55
+ args.apiSlug !== (current.apiSlug ?? current.fieldName)
56
+ ) {
57
+ throw new CrmAttributeValueError(
58
+ "crm-attribute-slug-immutable",
59
+ `The api slug of attribute ${args.attributeId} is "${current.apiSlug ?? current.fieldName}" and cannot change: every stored value row is keyed by it, so a rename would orphan the attribute's history. Create a new attribute instead.`,
60
+ );
61
+ }
62
+ if (args.type && args.type !== current.attributeType) {
63
+ throw new CrmAttributeValueError(
64
+ "crm-attribute-type-immutable",
65
+ `The type of attribute ${args.attributeId} is "${current.attributeType}" and cannot change: stored values live in the column that type chose, and reinterpreting them is a migration, not an edit. Create a new attribute and move the values.`,
66
+ );
67
+ }
68
+
69
+ const patch = {
70
+ ...(args.title === undefined ? {} : { label: args.title }),
71
+ ...(args.description === undefined
72
+ ? {}
73
+ : { description: args.description }),
74
+ ...(args.position === undefined ? {} : { position: args.position }),
75
+ ...(args.required === undefined ? {} : { required: args.required }),
76
+ ...(args.historyTracked === undefined
77
+ ? {}
78
+ : { historyTracked: args.historyTracked }),
79
+ ...(args.config === undefined
80
+ ? {}
81
+ : { configJson: toJson(args.config, MAX_CONFIG_CHARS) }),
82
+ ...(args.fillMode === undefined ? {} : { fillMode: args.fillMode }),
83
+ ...(args.fillConfig === undefined
84
+ ? {}
85
+ : { fillConfigJson: toJson(args.fillConfig, MAX_CONFIG_CHARS) }),
86
+ ...(args.archived === undefined ? {} : { archived: args.archived }),
87
+ };
88
+
89
+ const db = getDb();
90
+ if (Object.keys(patch).length > 0) {
91
+ await db
92
+ .update(schema.crmFieldPolicies)
93
+ .set({ ...patch, updatedAt: new Date().toISOString() })
94
+ .where(eq(schema.crmFieldPolicies.id, args.attributeId));
95
+ }
96
+
97
+ const [row] = await db
98
+ .select()
99
+ .from(schema.crmFieldPolicies)
100
+ .where(eq(schema.crmFieldPolicies.id, args.attributeId))
101
+ .limit(1);
102
+ if (!row) {
103
+ throw new Error("CRM attribute could not be verified after saving.");
104
+ }
105
+ const options = await loadAttributeOptions({
106
+ attributeIds: [row.id],
107
+ includeArchived: true,
108
+ });
109
+ return toAttributeDefinition(row, options.get(row.id) ?? []);
110
+ },
111
+ });
@@ -0,0 +1,102 @@
1
+ import { defineAction, type ActionRunContext } from "@agent-native/core/action";
2
+ import { accessFilter } 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 { requireCrmScope } from "./_crm-action-utils.js";
8
+ import {
9
+ crmActorFrom,
10
+ indexAttributes,
11
+ loadCrmListAttributes,
12
+ requireCrmList,
13
+ requireCrmListEntry,
14
+ writeCrmListEntryValues,
15
+ } from "./_crm-list-utils.js";
16
+ import { listEntryValuesSchema } from "./add-crm-record-to-list.js";
17
+
18
+ export default defineAction({
19
+ description:
20
+ "Set values on a CRM list entry's own attributes and/or reorder it. Moving an entry to another stage is exactly this call: the previous value's row is closed and a new one opened, so time-in-stage is derivable from the entry's own history. Entry attributes belong to the list, not to the record — this never writes to the record or to a provider.",
21
+ schema: z.object({
22
+ entryId: z.string().trim().min(1).max(128),
23
+ values: listEntryValuesSchema.optional(),
24
+ position: z.number().int().min(0).max(100_000).optional(),
25
+ }),
26
+ audit: {
27
+ target: (_args, result) => {
28
+ const entry = result as {
29
+ entryId: string;
30
+ ownerEmail: string;
31
+ orgId: string | null;
32
+ visibility: "private" | "org" | "public";
33
+ };
34
+ return {
35
+ type: "crm-list-entry",
36
+ id: entry.entryId,
37
+ ownerEmail: entry.ownerEmail,
38
+ orgId: entry.orgId,
39
+ visibility: entry.visibility,
40
+ };
41
+ },
42
+ summary: (args) => `Updated CRM list entry ${args.entryId}`,
43
+ recordInputs: false,
44
+ },
45
+ run: async (args, ctx?: ActionRunContext) => {
46
+ const db = getDb();
47
+ const entry = await requireCrmListEntry(db, args.entryId, "editor");
48
+ const list = await requireCrmList(db, entry.listId, "editor");
49
+ const attributes = indexAttributes(
50
+ await loadCrmListAttributes(db, list.id),
51
+ );
52
+ const actor = crmActorFrom(ctx);
53
+ const now = new Date().toISOString();
54
+ // Value rows inherit the ENTRY's ownership, not the caller's, so an editor
55
+ // writing to a shared entry cannot fork its visibility.
56
+ const ownership = {
57
+ ownerEmail: entry.ownerEmail,
58
+ orgId: entry.orgId,
59
+ visibility: entry.visibility,
60
+ };
61
+ // Reject an unauthenticated caller the same way a fresh write would.
62
+ requireCrmScope(ctx);
63
+
64
+ const writes = await db.transaction(async (tx) => {
65
+ if (args.position !== undefined) {
66
+ await tx
67
+ .update(schema.crmListEntries)
68
+ .set({ position: args.position, updatedAt: now })
69
+ .where(
70
+ and(
71
+ eq(schema.crmListEntries.id, entry.id),
72
+ accessFilter(
73
+ schema.crmListEntries,
74
+ schema.crmListEntryShares,
75
+ undefined,
76
+ "editor",
77
+ ),
78
+ ),
79
+ );
80
+ }
81
+ return writeCrmListEntryValues({
82
+ db: tx,
83
+ recordId: entry.recordId,
84
+ entryId: entry.id,
85
+ attributes,
86
+ values: args.values ?? {},
87
+ actor,
88
+ ownership,
89
+ now,
90
+ });
91
+ });
92
+
93
+ return {
94
+ entryId: entry.id,
95
+ listId: list.id,
96
+ recordId: entry.recordId,
97
+ position: args.position ?? entry.position,
98
+ values: writes,
99
+ ...ownership,
100
+ };
101
+ },
102
+ });