@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,634 @@
1
+ /**
2
+ * The one path a CRM `status` attribute value may be changed through.
3
+ *
4
+ * A status is not just another typed value: stages drive SLAs, boards, pipeline
5
+ * reporting, and celebration, so a blind status write is how a record silently
6
+ * re-enters a stage it already left, or lands in a retired one that no board
7
+ * column renders. The rules below are therefore stated as NAMED SETS with the
8
+ * reason each one exists, and every blocked case answers with a sentence a
9
+ * person can act on rather than a boolean the caller has to re-interpret.
10
+ *
11
+ * The allowed values come from the attribute's `crm_attribute_options` rows, not
12
+ * from an enum in this file — a workspace renames and retires its own stages.
13
+ *
14
+ * Two properties are load-bearing and easy to lose in a refactor:
15
+ *
16
+ * 1. Bulk transitions PARTITION by eligibility. An ineligible target is
17
+ * reported, never quietly written and never quietly dropped.
18
+ * 2. The observed `from` values are re-asserted in the claim query's WHERE
19
+ * clause, so a status that moved between the partition read and the write is
20
+ * left alone instead of being clobbered by a decision made about stale state.
21
+ */
22
+
23
+ import { accessFilter } from "@agent-native/core/sharing";
24
+ import { and, eq, inArray, isNull, or, type SQL } from "drizzle-orm";
25
+
26
+ import type { CrmActorType } from "../../shared/crm-contract.js";
27
+ import { schema } from "../db/index.js";
28
+ import {
29
+ CrmAttributeValueError,
30
+ writeCrmRecordField,
31
+ type CrmFieldWriteDb,
32
+ type CrmWritableAttribute,
33
+ } from "./record-fields.js";
34
+
35
+ /** Most targets one bulk transition may carry. */
36
+ export const MAX_STATUS_TRANSITION_TARGETS = 200;
37
+
38
+ /**
39
+ * A lifecycle problem the caller must fix before retrying. Shares the attribute
40
+ * writer's 422 contract so every "your input is wrong" failure out of the CRM
41
+ * write path looks the same to an HTTP, MCP, or agent caller.
42
+ */
43
+ export class CrmLifecycleError extends CrmAttributeValueError {
44
+ constructor(code: string, message: string) {
45
+ super(code, message);
46
+ this.name = "CrmLifecycleError";
47
+ }
48
+ }
49
+
50
+ export interface CrmStatusOption {
51
+ value: string;
52
+ title: string;
53
+ position: number;
54
+ archived: boolean;
55
+ celebrate: boolean;
56
+ targetDays: number | null;
57
+ }
58
+
59
+ export interface CrmStatusAttribute extends CrmWritableAttribute {
60
+ label: string;
61
+ authority: "provider" | "derived-local" | "local-authoritative";
62
+ archived: boolean;
63
+ }
64
+
65
+ export interface CrmLifecycle {
66
+ attribute: CrmStatusAttribute;
67
+ /** Every declared option, board order first. */
68
+ options: CrmStatusOption[];
69
+ /** Options a value may move INTO. */
70
+ enterableValues: string[];
71
+ /** Every declared value, archived included — what a stored value may be. */
72
+ knownValues: string[];
73
+ }
74
+
75
+ /** Why one target was not transitioned. Stable codes; sentences are for humans. */
76
+ export type CrmStatusBlockCode =
77
+ | "attribute-archived"
78
+ | "provider-authority"
79
+ | "record-tombstoned"
80
+ | "unknown-status"
81
+ | "archived-status"
82
+ | "concurrent-transition";
83
+
84
+ export interface CrmStatusBlock {
85
+ code: CrmStatusBlockCode;
86
+ message: string;
87
+ }
88
+
89
+ // ---------------------------------------------------------------------------
90
+ // Loading
91
+ // ---------------------------------------------------------------------------
92
+
93
+ /**
94
+ * Load one status attribute and its options.
95
+ *
96
+ * A non-`status` attribute is a caller bug, not a blocked transition: the
97
+ * lifecycle governs stages, and routing an ordinary text field through it would
98
+ * silently gain rules nothing declared.
99
+ */
100
+ export async function loadCrmStatusLifecycle(
101
+ db: CrmFieldWriteDb,
102
+ attributeId: string,
103
+ ): Promise<CrmLifecycle> {
104
+ const [row] = await db
105
+ .select({
106
+ id: schema.crmFieldPolicies.id,
107
+ apiSlug: schema.crmFieldPolicies.apiSlug,
108
+ fieldName: schema.crmFieldPolicies.fieldName,
109
+ label: schema.crmFieldPolicies.label,
110
+ attributeType: schema.crmFieldPolicies.attributeType,
111
+ multi: schema.crmFieldPolicies.multi,
112
+ historyTracked: schema.crmFieldPolicies.historyTracked,
113
+ valueType: schema.crmFieldPolicies.valueType,
114
+ storagePolicy: schema.crmFieldPolicies.storagePolicy,
115
+ authority: schema.crmFieldPolicies.authority,
116
+ archived: schema.crmFieldPolicies.archived,
117
+ })
118
+ .from(schema.crmFieldPolicies)
119
+ .where(
120
+ and(
121
+ eq(schema.crmFieldPolicies.id, attributeId),
122
+ accessFilter(schema.crmFieldPolicies, schema.crmFieldPolicyShares),
123
+ ),
124
+ )
125
+ .limit(1);
126
+
127
+ if (!row) {
128
+ throw new CrmLifecycleError(
129
+ "crm-status-attribute-not-found",
130
+ `CRM attribute "${attributeId}" was not found or you cannot see it.`,
131
+ );
132
+ }
133
+ if (row.attributeType !== "status") {
134
+ throw new CrmLifecycleError(
135
+ "crm-status-attribute-type",
136
+ `Attribute "${row.apiSlug ?? row.fieldName}" is a ${row.attributeType} attribute. Only a status attribute has a lifecycle.`,
137
+ );
138
+ }
139
+ if (
140
+ row.storagePolicy !== "mirrored" &&
141
+ row.storagePolicy !== "derived-local" &&
142
+ row.storagePolicy !== "local-authoritative"
143
+ ) {
144
+ throw new CrmLifecycleError(
145
+ "crm-status-attribute-not-writable",
146
+ `Attribute "${row.apiSlug ?? row.fieldName}" has storage policy "${row.storagePolicy}" and holds no local value to transition.`,
147
+ );
148
+ }
149
+
150
+ const optionRows = await db
151
+ .select({
152
+ value: schema.crmAttributeOptions.value,
153
+ title: schema.crmAttributeOptions.title,
154
+ position: schema.crmAttributeOptions.position,
155
+ archived: schema.crmAttributeOptions.archived,
156
+ celebrate: schema.crmAttributeOptions.celebrate,
157
+ targetDays: schema.crmAttributeOptions.targetDays,
158
+ })
159
+ .from(schema.crmAttributeOptions)
160
+ .where(
161
+ and(
162
+ eq(schema.crmAttributeOptions.attributeId, row.id),
163
+ accessFilter(
164
+ schema.crmAttributeOptions,
165
+ schema.crmAttributeOptionShares,
166
+ ),
167
+ ),
168
+ );
169
+
170
+ const options = optionRows
171
+ .map((option) => ({
172
+ value: option.value,
173
+ title: option.title,
174
+ position: option.position,
175
+ archived: option.archived,
176
+ celebrate: option.celebrate,
177
+ targetDays: option.targetDays,
178
+ }))
179
+ .sort((a, b) => a.position - b.position || a.value.localeCompare(b.value));
180
+
181
+ return {
182
+ attribute: {
183
+ id: row.id,
184
+ apiSlug: row.apiSlug ?? row.fieldName,
185
+ attributeType: "status",
186
+ multi: row.multi,
187
+ historyTracked: row.historyTracked,
188
+ valueType: row.valueType,
189
+ storagePolicy: row.storagePolicy,
190
+ fieldPolicyId: row.id,
191
+ label: row.label,
192
+ authority: row.authority,
193
+ archived: row.archived,
194
+ },
195
+ options,
196
+ // ENTERABLE, not "valid": an archived stage keeps every row already parked
197
+ // there (which is why leaving one is always allowed) but accepts nothing new.
198
+ enterableValues: options
199
+ .filter((option) => !option.archived)
200
+ .map((option) => option.value),
201
+ knownValues: options.map((option) => option.value),
202
+ };
203
+ }
204
+
205
+ // ---------------------------------------------------------------------------
206
+ // Rules
207
+ // ---------------------------------------------------------------------------
208
+
209
+ function quoteList(values: readonly string[]): string {
210
+ return values.length ? values.join(", ") : "(none defined)";
211
+ }
212
+
213
+ /**
214
+ * The reason this transition cannot happen, or `null` when it can.
215
+ *
216
+ * `from === to` is deliberately allowed: a repeat write is idempotent, and the
217
+ * bitemporal writer turns it into no write at all, so a bulk "move these to
218
+ * Won" over a set that already contains Won rows reports them as unchanged
219
+ * rather than blocked. Leaving ANY status — including an unknown or archived
220
+ * one — is allowed for the same reason a retired stage still holds rows: the
221
+ * way out of a bad state must not itself be blocked.
222
+ */
223
+ export function crmStatusBlockReason(
224
+ lifecycle: CrmLifecycle,
225
+ input: { from: string | null; to: string; recordTombstoned?: boolean },
226
+ ): CrmStatusBlock | null {
227
+ const label = lifecycle.attribute.label;
228
+
229
+ if (lifecycle.attribute.archived) {
230
+ return {
231
+ code: "attribute-archived",
232
+ message: `Cannot set "${label}" — the attribute is archived. Unarchive it before moving anything through it.`,
233
+ };
234
+ }
235
+ if (lifecycle.attribute.authority === "provider") {
236
+ return {
237
+ code: "provider-authority",
238
+ message: `Cannot set "${label}" locally — the connected provider owns this attribute. Prepare a change and complete it in the provider instead.`,
239
+ };
240
+ }
241
+ if (input.recordTombstoned) {
242
+ return {
243
+ code: "record-tombstoned",
244
+ message: `Cannot move a merged or deleted record through "${label}".`,
245
+ };
246
+ }
247
+ if (!lifecycle.knownValues.includes(input.to)) {
248
+ return {
249
+ code: "unknown-status",
250
+ message: `"${input.to}" is not a value of "${label}". Known values: ${quoteList(lifecycle.knownValues)}. Add the option before moving anything into it.`,
251
+ };
252
+ }
253
+ if (!lifecycle.enterableValues.includes(input.to)) {
254
+ return {
255
+ code: "archived-status",
256
+ message: `Cannot move into "${input.to}" — that value of "${label}" is archived. Pick one of: ${quoteList(lifecycle.enterableValues)}.`,
257
+ };
258
+ }
259
+ return null;
260
+ }
261
+
262
+ // ---------------------------------------------------------------------------
263
+ // Reading current values
264
+ // ---------------------------------------------------------------------------
265
+
266
+ export interface CrmStatusTarget {
267
+ recordId: string;
268
+ /** Set for a LIST-ENTRY status; omitted or null for a record status. */
269
+ entryId?: string | null;
270
+ }
271
+
272
+ interface CurrentStatus {
273
+ fieldId: string;
274
+ value: string | null;
275
+ }
276
+
277
+ function targetKey(target: CrmStatusTarget): string {
278
+ return target.entryId
279
+ ? `entry:${target.entryId}`
280
+ : `record:${target.recordId}`;
281
+ }
282
+
283
+ /**
284
+ * Current status value per target. A target with no row yet maps to `null`,
285
+ * which is a real state (never set) and distinct from a target that is missing
286
+ * from the map entirely (not visible to this caller).
287
+ */
288
+ async function readCurrentStatuses(
289
+ db: CrmFieldWriteDb,
290
+ lifecycle: CrmLifecycle,
291
+ targets: CrmStatusTarget[],
292
+ extraWhere?: SQL,
293
+ ): Promise<Map<string, CurrentStatus>> {
294
+ const entryIds = targets
295
+ .map((target) => target.entryId)
296
+ .filter((entryId): entryId is string => Boolean(entryId));
297
+ const recordIds = targets
298
+ .filter((target) => !target.entryId)
299
+ .map((target) => target.recordId);
300
+
301
+ const shapes: SQL[] = [];
302
+ if (entryIds.length) {
303
+ shapes.push(inArray(schema.crmRecordFields.entryId, entryIds) as SQL);
304
+ }
305
+ if (recordIds.length) {
306
+ shapes.push(
307
+ and(
308
+ inArray(schema.crmRecordFields.recordId, recordIds),
309
+ isNull(schema.crmRecordFields.entryId),
310
+ ) as SQL,
311
+ );
312
+ }
313
+ if (!shapes.length) return new Map();
314
+
315
+ const rows = await db
316
+ .select({
317
+ id: schema.crmRecordFields.id,
318
+ recordId: schema.crmRecordFields.recordId,
319
+ entryId: schema.crmRecordFields.entryId,
320
+ stringValue: schema.crmRecordFields.stringValue,
321
+ })
322
+ .from(schema.crmRecordFields)
323
+ .where(
324
+ and(
325
+ or(...shapes),
326
+ eq(schema.crmRecordFields.fieldName, lifecycle.attribute.apiSlug),
327
+ isNull(schema.crmRecordFields.activeUntil),
328
+ ...(extraWhere ? [extraWhere] : []),
329
+ accessFilter(schema.crmRecordFields, schema.crmRecordFieldShares),
330
+ ),
331
+ );
332
+
333
+ const byTarget = new Map<string, CurrentStatus>();
334
+ for (const row of rows) {
335
+ byTarget.set(targetKey({ recordId: row.recordId, entryId: row.entryId }), {
336
+ fieldId: row.id,
337
+ value: row.stringValue,
338
+ });
339
+ }
340
+ return byTarget;
341
+ }
342
+
343
+ /**
344
+ * Re-read the targets whose status still equals what the partition observed.
345
+ *
346
+ * This is the concurrency guard: the observed `from` values go into the WHERE
347
+ * clause, so a target somebody else moved in between does not come back and is
348
+ * therefore never written with a decision made about its old state.
349
+ *
350
+ * ponytail: inside one transaction this is exact on SQLite/libSQL, which
351
+ * serializes writers. On Postgres read-committed a writer could still commit
352
+ * between this SELECT and the write; move to `SELECT … FOR UPDATE` behind a
353
+ * dialect helper in core if a hosted deployment shows clobbered stage history.
354
+ */
355
+ export async function claimCrmStatusTransition(input: {
356
+ db: CrmFieldWriteDb;
357
+ lifecycle: CrmLifecycle;
358
+ /** Targets to claim, each with the status value the decision was based on. */
359
+ expected: Array<{ target: CrmStatusTarget; from: string | null }>;
360
+ }): Promise<Set<string>> {
361
+ const withValue = input.expected.filter((entry) => entry.from !== null);
362
+ const withoutValue = input.expected.filter((entry) => entry.from === null);
363
+
364
+ const claimed = new Set<string>();
365
+
366
+ if (withValue.length) {
367
+ const observed = await readCurrentStatuses(
368
+ input.db,
369
+ input.lifecycle,
370
+ withValue.map((entry) => entry.target),
371
+ inArray(schema.crmRecordFields.stringValue, [
372
+ ...new Set(withValue.map((entry) => entry.from as string)),
373
+ ]) as SQL,
374
+ );
375
+ for (const entry of withValue) {
376
+ const key = targetKey(entry.target);
377
+ if (observed.get(key)?.value === entry.from) claimed.add(key);
378
+ }
379
+ }
380
+
381
+ if (withoutValue.length) {
382
+ // "No row" cannot be asserted by matching a value, so it is asserted by its
383
+ // absence: a target that has grown a status row since the partition read is
384
+ // not claimed.
385
+ const observed = await readCurrentStatuses(
386
+ input.db,
387
+ input.lifecycle,
388
+ withoutValue.map((entry) => entry.target),
389
+ );
390
+ for (const entry of withoutValue) {
391
+ const key = targetKey(entry.target);
392
+ if (!observed.has(key)) claimed.add(key);
393
+ }
394
+ }
395
+
396
+ return claimed;
397
+ }
398
+
399
+ // ---------------------------------------------------------------------------
400
+ // Bulk transition
401
+ // ---------------------------------------------------------------------------
402
+
403
+ export type CrmStatusOutcome = "changed" | "unchanged" | "skipped";
404
+
405
+ export interface CrmStatusTransitionRow {
406
+ recordId: string;
407
+ entryId: string | null;
408
+ from: string | null;
409
+ to: string;
410
+ outcome: CrmStatusOutcome;
411
+ /** Present on every `skipped` row; absent otherwise. */
412
+ block?: CrmStatusBlock;
413
+ /** Present on every `changed` row: how the bitemporal writer stored it. */
414
+ mode?: "insert" | "close-and-insert" | "update-in-place";
415
+ }
416
+
417
+ export interface CrmStatusTransitionReport {
418
+ attributeId: string;
419
+ apiSlug: string;
420
+ to: string;
421
+ changed: number;
422
+ unchanged: number;
423
+ skipped: number;
424
+ /** Skipped counts keyed by the status they were still in — GTM's shape. */
425
+ skippedByStatus: Record<string, number>;
426
+ skippedByReason: Record<CrmStatusBlockCode, number>;
427
+ rows: CrmStatusTransitionRow[];
428
+ }
429
+
430
+ export interface CrmStatusTransitionInput {
431
+ db: CrmFieldWriteDb;
432
+ lifecycle: CrmLifecycle;
433
+ targets: CrmStatusTarget[];
434
+ to: string;
435
+ actor: { type: CrmActorType; id?: string | null };
436
+ ownership: {
437
+ ownerEmail: string;
438
+ orgId: string | null;
439
+ visibility: "private" | "org" | "public";
440
+ };
441
+ /** Record ids known to be tombstoned; those targets are reported, not written. */
442
+ tombstonedRecordIds?: ReadonlySet<string>;
443
+ now?: string;
444
+ }
445
+
446
+ /**
447
+ * Move a bounded set of records or list entries into one status.
448
+ *
449
+ * Eligible targets are written through `writeCrmRecordField`, so a real move
450
+ * closes the previous value's row and opens a new one — that pair IS the
451
+ * time-in-stage history. Ineligible targets come back in `rows` with the
452
+ * sentence explaining why; nothing is written for them and nothing is dropped.
453
+ */
454
+ export async function applyCrmStatusTransitions(
455
+ input: CrmStatusTransitionInput,
456
+ ): Promise<CrmStatusTransitionReport> {
457
+ if (input.targets.length > MAX_STATUS_TRANSITION_TARGETS) {
458
+ throw new CrmLifecycleError(
459
+ "crm-status-transition-too-many",
460
+ `A status transition may cover at most ${MAX_STATUS_TRANSITION_TARGETS} targets; received ${input.targets.length}.`,
461
+ );
462
+ }
463
+
464
+ const now = input.now ?? new Date().toISOString();
465
+ const tombstoned = input.tombstonedRecordIds ?? new Set<string>();
466
+ const current = await readCurrentStatuses(
467
+ input.db,
468
+ input.lifecycle,
469
+ input.targets,
470
+ );
471
+
472
+ const rows: CrmStatusTransitionRow[] = [];
473
+ const eligible: Array<{ target: CrmStatusTarget; from: string | null }> = [];
474
+
475
+ for (const target of input.targets) {
476
+ const from = current.get(targetKey(target))?.value ?? null;
477
+ const block = crmStatusBlockReason(input.lifecycle, {
478
+ from,
479
+ to: input.to,
480
+ recordTombstoned: tombstoned.has(target.recordId),
481
+ });
482
+ if (block) {
483
+ rows.push({
484
+ recordId: target.recordId,
485
+ entryId: target.entryId ?? null,
486
+ from,
487
+ to: input.to,
488
+ outcome: "skipped",
489
+ block,
490
+ });
491
+ continue;
492
+ }
493
+ eligible.push({ target, from });
494
+ }
495
+
496
+ const claimed = eligible.length
497
+ ? await claimCrmStatusTransition({
498
+ db: input.db,
499
+ lifecycle: input.lifecycle,
500
+ expected: eligible,
501
+ })
502
+ : new Set<string>();
503
+
504
+ for (const entry of eligible) {
505
+ const key = targetKey(entry.target);
506
+ if (!claimed.has(key)) {
507
+ rows.push({
508
+ recordId: entry.target.recordId,
509
+ entryId: entry.target.entryId ?? null,
510
+ from: entry.from,
511
+ to: input.to,
512
+ outcome: "skipped",
513
+ block: {
514
+ code: "concurrent-transition",
515
+ message: `"${input.lifecycle.attribute.label}" changed from "${entry.from ?? "(not set)"}" while this move was being prepared, so it was left alone. Re-read it and decide again.`,
516
+ },
517
+ });
518
+ continue;
519
+ }
520
+
521
+ const result = await writeCrmRecordField({
522
+ db: input.db,
523
+ target: {
524
+ recordId: entry.target.recordId,
525
+ entryId: entry.target.entryId ?? null,
526
+ },
527
+ attribute: input.lifecycle.attribute,
528
+ value: input.to,
529
+ actor: input.actor,
530
+ ownership: input.ownership,
531
+ now,
532
+ });
533
+ rows.push({
534
+ recordId: entry.target.recordId,
535
+ entryId: entry.target.entryId ?? null,
536
+ from: entry.from,
537
+ to: input.to,
538
+ outcome: result.changed ? "changed" : "unchanged",
539
+ ...(result.changed ? { mode: result.mode } : {}),
540
+ });
541
+ }
542
+
543
+ const skippedByStatus: Record<string, number> = {};
544
+ const skippedByReason = {} as Record<CrmStatusBlockCode, number>;
545
+ for (const row of rows) {
546
+ if (row.outcome !== "skipped" || !row.block) continue;
547
+ const status = row.from ?? "(not set)";
548
+ skippedByStatus[status] = (skippedByStatus[status] ?? 0) + 1;
549
+ skippedByReason[row.block.code] =
550
+ (skippedByReason[row.block.code] ?? 0) + 1;
551
+ }
552
+
553
+ return {
554
+ attributeId: input.lifecycle.attribute.id,
555
+ apiSlug: input.lifecycle.attribute.apiSlug,
556
+ to: input.to,
557
+ changed: rows.filter((row) => row.outcome === "changed").length,
558
+ unchanged: rows.filter((row) => row.outcome === "unchanged").length,
559
+ skipped: rows.filter((row) => row.outcome === "skipped").length,
560
+ skippedByStatus,
561
+ skippedByReason,
562
+ rows,
563
+ };
564
+ }
565
+
566
+ // ---------------------------------------------------------------------------
567
+ // Single-target entry points
568
+ // ---------------------------------------------------------------------------
569
+
570
+ /**
571
+ * Move one record or list entry into a status.
572
+ *
573
+ * A single-target caller has nowhere to put a partition report, so a blocked
574
+ * target is raised as the sentence it must act on rather than returned as a row
575
+ * nobody reads — including the concurrency block, whose whole point is that the
576
+ * caller re-reads and decides again.
577
+ */
578
+ export async function applyOneCrmStatusTransition(
579
+ input: Omit<CrmStatusTransitionInput, "targets"> & {
580
+ target: CrmStatusTarget;
581
+ },
582
+ ): Promise<{
583
+ changed: boolean;
584
+ mode?: "insert" | "close-and-insert" | "update-in-place";
585
+ }> {
586
+ const { target, ...rest } = input;
587
+ const report = await applyCrmStatusTransitions({
588
+ ...rest,
589
+ targets: [target],
590
+ });
591
+ const [row] = report.rows;
592
+ if (!row) {
593
+ throw new CrmLifecycleError(
594
+ "crm-status-transition-unreported",
595
+ `"${input.lifecycle.attribute.label}" was neither transitioned nor blocked for ${
596
+ target.entryId ? `entry ${target.entryId}` : `record ${target.recordId}`
597
+ }. Nothing was written; report this rather than retrying.`,
598
+ );
599
+ }
600
+ if (row.block) throw new CrmLifecycleError(row.block.code, row.block.message);
601
+ return {
602
+ changed: row.outcome === "changed",
603
+ ...(row.mode ? { mode: row.mode } : {}),
604
+ };
605
+ }
606
+
607
+ /**
608
+ * Throw the reason this target cannot enter `to`, or return the value it would
609
+ * move from.
610
+ *
611
+ * The gate half, for a caller that owns its own atomic write and its own
612
+ * concurrency check: `update-crm-record` is one revision-checked mutation with
613
+ * an audit row covering the whole patch, so routing its status fields into a
614
+ * second writer would leave that row claiming fields it did not write.
615
+ */
616
+ export async function assertCrmStatusTransitionAllowed(input: {
617
+ db: CrmFieldWriteDb;
618
+ lifecycle: CrmLifecycle;
619
+ target: CrmStatusTarget;
620
+ to: string;
621
+ recordTombstoned?: boolean;
622
+ }): Promise<{ from: string | null }> {
623
+ const current = await readCurrentStatuses(input.db, input.lifecycle, [
624
+ input.target,
625
+ ]);
626
+ const from = current.get(targetKey(input.target))?.value ?? null;
627
+ const block = crmStatusBlockReason(input.lifecycle, {
628
+ from,
629
+ to: input.to,
630
+ recordTombstoned: input.recordTombstoned,
631
+ });
632
+ if (block) throw new CrmLifecycleError(block.code, block.message);
633
+ return { from };
634
+ }
@@ -0,0 +1,64 @@
1
+ import {
2
+ createProviderApiRuntime,
3
+ type ProviderApiCredentialResolver,
4
+ type ProviderApiId,
5
+ } from "@agent-native/core/provider-api";
6
+ import { getCredentialContext } from "@agent-native/core/server";
7
+ import { resolveWorkspaceConnectionCredentialForApp } from "@agent-native/core/workspace-connections";
8
+
9
+ export const CRM_APP_ID = "crm";
10
+ export const CRM_PROVIDER_API_IDS = ["hubspot", "salesforce"] as [
11
+ ProviderApiId,
12
+ ProviderApiId,
13
+ ];
14
+
15
+ const resolveCrmProviderCredential: ProviderApiCredentialResolver = async ({
16
+ provider,
17
+ key,
18
+ ctx,
19
+ workspaceProvider,
20
+ connectionId,
21
+ }) => {
22
+ const resolvedProvider = workspaceProvider ?? provider;
23
+ const credential = await resolveWorkspaceConnectionCredentialForApp({
24
+ appId: CRM_APP_ID,
25
+ provider: resolvedProvider,
26
+ key,
27
+ ...(connectionId ? { connectionId } : {}),
28
+ userEmail: ctx.userEmail,
29
+ orgId: ctx.orgId ?? null,
30
+ });
31
+ if (!credential.available || !credential.value || !credential.provenance) {
32
+ return null;
33
+ }
34
+
35
+ return {
36
+ key: credential.provenance.resolvedKey,
37
+ value: credential.value,
38
+ source: "workspace_connection",
39
+ provider: resolvedProvider,
40
+ connectionId: credential.provenance.connectionId,
41
+ connectionLabel: credential.provenance.connectionLabel,
42
+ scope: credential.provenance.secretScope,
43
+ };
44
+ };
45
+
46
+ const runtime = createProviderApiRuntime({
47
+ appId: CRM_APP_ID,
48
+ providerIds: CRM_PROVIDER_API_IDS,
49
+ localCredentialSource: "crm_workspace_connection",
50
+ getCredentialContext: () => {
51
+ const context = getCredentialContext();
52
+ if (!context) {
53
+ throw new Error(
54
+ "CRM provider API requests require an authenticated request context.",
55
+ );
56
+ }
57
+ return context;
58
+ },
59
+ resolveCredential: resolveCrmProviderCredential,
60
+ });
61
+
62
+ export function getCrmProviderApiRuntime() {
63
+ return runtime;
64
+ }
@@ -0,0 +1,4 @@
1
+ import { runAuthGuard } from "@agent-native/core/server";
2
+ import { defineEventHandler } from "h3";
3
+
4
+ export default defineEventHandler(async (event) => runAuthGuard(event));