@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,519 @@
1
+ /**
2
+ * Named enrichment provider slots.
3
+ *
4
+ * A slot is a capability the CRM buys — "who is this company", "what is this
5
+ * person's work email" — bound to exactly one provider in the registry below.
6
+ * Nothing outside this file names a vendor: not an action schema, not a wire
7
+ * field, not a UI string. Swapping a provider is one line here.
8
+ *
9
+ * Every slot answers with a five-state outcome, and the states stay separate all
10
+ * the way to the grid. A slot with no credential, a slot the record cannot be
11
+ * asked (no domain to look up), a slot that answered with nothing, and a slot
12
+ * that threw are four different facts about the world. Collapsing any pair of
13
+ * them into "no data" is exactly the failure the root CLAUDE.md names: the UI
14
+ * would then report something confidently wrong and nobody could see it.
15
+ */
16
+
17
+ import {
18
+ createProviderApiRuntime,
19
+ type ProviderApiId,
20
+ type ProviderApiRequestArgs,
21
+ } from "@agent-native/core/provider-api";
22
+ import { getCredentialContext } from "@agent-native/core/server";
23
+ import { resolveWorkspaceConnectionCredentialForApp } from "@agent-native/core/workspace-connections";
24
+
25
+ import { CRM_APP_ID } from "./provider-api.js";
26
+
27
+ export const CRM_ENRICHMENT_SLOTS = [
28
+ "company",
29
+ "person",
30
+ "contact",
31
+ "web",
32
+ "calls",
33
+ ] as const;
34
+
35
+ export type CrmEnrichmentSlot = (typeof CRM_ENRICHMENT_SLOTS)[number];
36
+
37
+ export function isCrmEnrichmentSlot(
38
+ value: unknown,
39
+ ): value is CrmEnrichmentSlot {
40
+ return (
41
+ typeof value === "string" &&
42
+ (CRM_ENRICHMENT_SLOTS as readonly string[]).includes(value)
43
+ );
44
+ }
45
+
46
+ /** What a slot can be asked about one record. */
47
+ export interface CrmEnrichmentTarget {
48
+ recordId: string;
49
+ displayName: string;
50
+ domain?: string | null;
51
+ firstName?: string | null;
52
+ lastName?: string | null;
53
+ companyName?: string | null;
54
+ }
55
+
56
+ /** One normalized value a slot produced, with the page it came from. */
57
+ export interface CrmSlotFact {
58
+ key: string;
59
+ value: string | number | boolean;
60
+ sourceUrl?: string;
61
+ }
62
+
63
+ export type CrmEnrichmentSlotOutcome =
64
+ /** No credential is granted for this slot. Nothing was called, nothing spent. */
65
+ | { slot: CrmEnrichmentSlot; status: "unconfigured"; reason: string }
66
+ /** The record lacks the identifier this slot resolves against. Not called. */
67
+ | { slot: CrmEnrichmentSlot; status: "skipped"; reason: string }
68
+ /** Called, answered, carried facts. */
69
+ | {
70
+ slot: CrmEnrichmentSlot;
71
+ status: "ok";
72
+ facts: CrmSlotFact[];
73
+ observedAt: string;
74
+ }
75
+ /** Called, answered, carried nothing. A real (and different) answer. */
76
+ | { slot: CrmEnrichmentSlot; status: "empty"; observedAt: string }
77
+ /** Called and threw. The provider's message is preserved verbatim. */
78
+ | { slot: CrmEnrichmentSlot; status: "error"; error: string };
79
+
80
+ export type CrmEnrichmentPhase = "verify" | "spend";
81
+
82
+ /** Longest fact string persisted; facts are metadata, never payload bodies. */
83
+ const MAX_FACT_CHARS = 500;
84
+ const MAX_FACTS_PER_SLOT = 20;
85
+
86
+ interface CrmEnrichmentSlotDriver {
87
+ provider: ProviderApiId;
88
+ /** Credential the slot needs. Checked for presence; its value is never read here. */
89
+ credentialKey: string;
90
+ /**
91
+ * True when the response can carry an email or phone number. Phase A (the
92
+ * unpaid verification pass) may never run one of these — see `verifySlots`.
93
+ */
94
+ carriesContactData: boolean;
95
+ /** Normalized fact key -> dot path into the provider response body. */
96
+ factPaths: Readonly<Record<string, string>>;
97
+ /** null when the target lacks the identifier this provider resolves against. */
98
+ buildRequest(target: CrmEnrichmentTarget): ProviderApiRequestArgs | null;
99
+ }
100
+
101
+ /**
102
+ * The registry. One line per slot.
103
+ *
104
+ * ponytail: the request recipes are each provider's documented entry point, not
105
+ * a tuned per-tenant query — the ceiling is recall, not correctness. A tenant
106
+ * that needs a narrower query refines it through `provider-api-docs` and
107
+ * `provider-api-request` rather than by editing this table.
108
+ */
109
+ const SLOT_DRIVERS: Record<CrmEnrichmentSlot, CrmEnrichmentSlotDriver> = {
110
+ company: {
111
+ provider: "apollo",
112
+ credentialKey: "APOLLO_API_KEY",
113
+ carriesContactData: false,
114
+ factPaths: {
115
+ industry: "organization.industry",
116
+ employeeCount: "organization.estimated_num_employees",
117
+ country: "organization.country",
118
+ city: "organization.city",
119
+ foundedYear: "organization.founded_year",
120
+ websiteUrl: "organization.website_url",
121
+ linkedinUrl: "organization.linkedin_url",
122
+ description: "organization.short_description",
123
+ },
124
+ buildRequest: (target) =>
125
+ target.domain
126
+ ? {
127
+ provider: "apollo",
128
+ method: "GET",
129
+ path: "/api/v1/organizations/enrich",
130
+ query: { domain: target.domain },
131
+ }
132
+ : null,
133
+ },
134
+ person: {
135
+ provider: "apollo",
136
+ credentialKey: "APOLLO_API_KEY",
137
+ carriesContactData: false,
138
+ factPaths: {
139
+ title: "person.title",
140
+ seniority: "person.seniority",
141
+ linkedinUrl: "person.linkedin_url",
142
+ city: "person.city",
143
+ country: "person.country",
144
+ organizationName: "person.organization.name",
145
+ },
146
+ buildRequest: (target) =>
147
+ target.firstName && (target.domain || target.companyName)
148
+ ? {
149
+ provider: "apollo",
150
+ method: "POST",
151
+ path: "/api/v1/people/match",
152
+ body: {
153
+ first_name: target.firstName,
154
+ last_name: target.lastName ?? undefined,
155
+ domain: target.domain ?? undefined,
156
+ organization_name: target.companyName ?? undefined,
157
+ },
158
+ }
159
+ : null,
160
+ },
161
+ contact: {
162
+ provider: "apollo",
163
+ credentialKey: "APOLLO_API_KEY",
164
+ // The reveal flags below are what makes this request billable, and they are
165
+ // the only reason phase A exists.
166
+ carriesContactData: true,
167
+ factPaths: {
168
+ email: "person.email",
169
+ phone: "person.sanitized_phone",
170
+ },
171
+ buildRequest: (target) =>
172
+ target.firstName && (target.domain || target.companyName)
173
+ ? {
174
+ provider: "apollo",
175
+ method: "POST",
176
+ path: "/api/v1/people/match",
177
+ body: {
178
+ first_name: target.firstName,
179
+ last_name: target.lastName ?? undefined,
180
+ domain: target.domain ?? undefined,
181
+ organization_name: target.companyName ?? undefined,
182
+ reveal_personal_emails: true,
183
+ reveal_phone_number: true,
184
+ },
185
+ }
186
+ : null,
187
+ },
188
+ web: {
189
+ provider: "dataforseo",
190
+ credentialKey: "DATAFORSEO_LOGIN",
191
+ carriesContactData: false,
192
+ factPaths: {
193
+ topResultTitle: "tasks.0.result.0.items.0.title",
194
+ topResultUrl: "tasks.0.result.0.items.0.url",
195
+ topResultSnippet: "tasks.0.result.0.items.0.description",
196
+ },
197
+ buildRequest: (target) =>
198
+ target.companyName || target.displayName
199
+ ? {
200
+ provider: "dataforseo",
201
+ method: "POST",
202
+ path: "/serp/google/organic/live/advanced",
203
+ body: [
204
+ {
205
+ keyword: `${target.companyName ?? target.displayName} company news`,
206
+ language_code: "en",
207
+ location_code: 2840,
208
+ depth: 10,
209
+ },
210
+ ],
211
+ }
212
+ : null,
213
+ },
214
+ calls: {
215
+ provider: "gong",
216
+ credentialKey: "GONG_ACCESS_KEY",
217
+ carriesContactData: false,
218
+ factPaths: {
219
+ lastCallTitle: "calls.0.metaData.title",
220
+ lastCallStartedAt: "calls.0.metaData.started",
221
+ lastCallUrl: "calls.0.metaData.url",
222
+ },
223
+ buildRequest: (target) =>
224
+ target.domain
225
+ ? {
226
+ provider: "gong",
227
+ method: "POST",
228
+ path: "/calls/extensive",
229
+ body: {
230
+ filter: { companyIds: [target.domain] },
231
+ contentSelector: { exposedFields: { parties: true } },
232
+ },
233
+ }
234
+ : null,
235
+ },
236
+ };
237
+
238
+ const ENRICHMENT_PROVIDER_IDS = Array.from(
239
+ new Set(Object.values(SLOT_DRIVERS).map((driver) => driver.provider)),
240
+ ) as ProviderApiId[];
241
+
242
+ // A second runtime rather than the CRM one: `server/lib/provider-api.ts` is the
243
+ // user-facing HubSpot/Salesforce escape hatch, and widening its allow-list would
244
+ // let `provider-api-request` reach enrichment vendors it has no business
245
+ // reaching. Same app id, so credentials still resolve through the same grants.
246
+ const enrichmentRuntime = createProviderApiRuntime({
247
+ appId: CRM_APP_ID,
248
+ providerIds: ENRICHMENT_PROVIDER_IDS,
249
+ localCredentialSource: "crm_workspace_connection",
250
+ getCredentialContext: () => {
251
+ const context = getCredentialContext();
252
+ if (!context) {
253
+ throw new Error(
254
+ "CRM enrichment requires an authenticated request context.",
255
+ );
256
+ }
257
+ return context;
258
+ },
259
+ });
260
+
261
+ /** Seam the tests drive; production binds it to the provider API substrate. */
262
+ export interface CrmEnrichmentSlotDeps {
263
+ checkCredential(input: {
264
+ provider: ProviderApiId;
265
+ key: string;
266
+ }): Promise<{ available: boolean; reason: string }>;
267
+ execute(args: ProviderApiRequestArgs): Promise<unknown>;
268
+ }
269
+
270
+ export const providerApiSlotDeps: CrmEnrichmentSlotDeps = {
271
+ checkCredential: async ({ provider, key }) => {
272
+ const context = getCredentialContext();
273
+ if (!context) {
274
+ throw new Error(
275
+ "CRM enrichment requires an authenticated request context.",
276
+ );
277
+ }
278
+ const resolution = await resolveWorkspaceConnectionCredentialForApp({
279
+ appId: CRM_APP_ID,
280
+ provider,
281
+ key,
282
+ userEmail: context.userEmail,
283
+ orgId: context.orgId ?? null,
284
+ });
285
+ // Deliberately does not read `resolution.value` — presence is the question.
286
+ // The resolver's own `reason` names the provider ("no available apollo
287
+ // workspace connection…"), which would put a vendor on the wire; its
288
+ // `status` is a vendor-free enum and carries the same distinction.
289
+ return {
290
+ available: resolution.available,
291
+ reason: resolution.available
292
+ ? resolution.status
293
+ : `No workspace connection backs this slot (${resolution.status}). Grant one to CRM in workspace connection settings.`,
294
+ };
295
+ },
296
+ execute: (args) => enrichmentRuntime.executeRequest(args),
297
+ };
298
+
299
+ /**
300
+ * Whether a slot can be used right now.
301
+ *
302
+ * `unknown` is not a synonym for `missing`: a credential lookup that itself
303
+ * failed tells us nothing about whether the credential exists, and rendering
304
+ * that as "not connected" sends the user to configure something already set up.
305
+ */
306
+ export type CrmEnrichmentSlotCredential =
307
+ | { status: "granted" }
308
+ | { status: "missing"; reason: string }
309
+ | { status: "unknown"; error: string };
310
+
311
+ export interface CrmEnrichmentSlotDescription {
312
+ slot: CrmEnrichmentSlot;
313
+ /** Which pass the slot belongs to: the free evidence pass or the paid one. */
314
+ phase: CrmEnrichmentPhase;
315
+ carriesContactData: boolean;
316
+ /** Normalized fact keys the slot can produce. Never provider field names. */
317
+ factKeys: string[];
318
+ credential: CrmEnrichmentSlotCredential;
319
+ }
320
+
321
+ /** Every slot with its live credential state. Reads nothing, spends nothing. */
322
+ export async function describeEnrichmentSlots(
323
+ deps: CrmEnrichmentSlotDeps = providerApiSlotDeps,
324
+ ): Promise<CrmEnrichmentSlotDescription[]> {
325
+ return Promise.all(
326
+ CRM_ENRICHMENT_SLOTS.map(async (slot) => {
327
+ const driver = SLOT_DRIVERS[slot];
328
+ let credential: CrmEnrichmentSlotCredential;
329
+ try {
330
+ const resolved = await deps.checkCredential({
331
+ provider: driver.provider,
332
+ key: driver.credentialKey,
333
+ });
334
+ credential = resolved.available
335
+ ? { status: "granted" }
336
+ : { status: "missing", reason: resolved.reason };
337
+ } catch (error) {
338
+ credential = { status: "unknown", error: messageOf(error) };
339
+ }
340
+ return {
341
+ slot,
342
+ phase: driver.carriesContactData
343
+ ? ("spend" as const)
344
+ : ("verify" as const),
345
+ carriesContactData: driver.carriesContactData,
346
+ factKeys: Object.keys(driver.factPaths),
347
+ credential,
348
+ };
349
+ }),
350
+ );
351
+ }
352
+
353
+ /**
354
+ * The slots phase A may run: every requested slot that cannot carry contact
355
+ * data. Phase A produces evidence a human approves; it must never be the pass
356
+ * that buys an email, or the approval gate guards nothing.
357
+ */
358
+ export function verifySlots(
359
+ requested: readonly CrmEnrichmentSlot[],
360
+ ): CrmEnrichmentSlot[] {
361
+ return requested.filter((slot) => !SLOT_DRIVERS[slot].carriesContactData);
362
+ }
363
+
364
+ /** The slots phase B pays for: the contact-bearing ones among those requested. */
365
+ export function spendSlots(
366
+ requested: readonly CrmEnrichmentSlot[],
367
+ ): CrmEnrichmentSlot[] {
368
+ return requested.filter((slot) => SLOT_DRIVERS[slot].carriesContactData);
369
+ }
370
+
371
+ export function slotCarriesContactData(slot: CrmEnrichmentSlot): boolean {
372
+ return SLOT_DRIVERS[slot].carriesContactData;
373
+ }
374
+
375
+ /** Value at a dot path, walking objects and numeric array indexes. */
376
+ function readPath(body: unknown, path: string): unknown {
377
+ let cursor: unknown = body;
378
+ for (const segment of path.split(".")) {
379
+ if (cursor === null || cursor === undefined) return undefined;
380
+ if (Array.isArray(cursor)) {
381
+ const index = Number(segment);
382
+ if (!Number.isInteger(index)) return undefined;
383
+ cursor = cursor[index];
384
+ continue;
385
+ }
386
+ if (typeof cursor !== "object") return undefined;
387
+ cursor = (cursor as Record<string, unknown>)[segment];
388
+ }
389
+ return cursor;
390
+ }
391
+
392
+ /**
393
+ * The provider's response body. `executeRequest` wraps it, and the wrapper shape
394
+ * varies by staging options, so unwrap what is there rather than assuming.
395
+ */
396
+ function responseBody(response: unknown): unknown {
397
+ if (response && typeof response === "object" && !Array.isArray(response)) {
398
+ const record = response as Record<string, unknown>;
399
+ if ("body" in record) return record.body;
400
+ if ("data" in record) return record.data;
401
+ }
402
+ return response;
403
+ }
404
+
405
+ export function extractSlotFacts(
406
+ slot: CrmEnrichmentSlot,
407
+ response: unknown,
408
+ ): CrmSlotFact[] {
409
+ const body = responseBody(response);
410
+ const facts: CrmSlotFact[] = [];
411
+ for (const [key, path] of Object.entries(SLOT_DRIVERS[slot].factPaths)) {
412
+ if (facts.length >= MAX_FACTS_PER_SLOT) break;
413
+ const value = readPath(body, path);
414
+ if (typeof value === "string") {
415
+ const trimmed = value.trim();
416
+ if (trimmed) facts.push({ key, value: trimmed.slice(0, MAX_FACT_CHARS) });
417
+ continue;
418
+ }
419
+ if (typeof value === "number" && Number.isFinite(value)) {
420
+ facts.push({ key, value });
421
+ continue;
422
+ }
423
+ if (typeof value === "boolean") facts.push({ key, value });
424
+ }
425
+ return facts;
426
+ }
427
+
428
+ /**
429
+ * Run one slot against one record.
430
+ *
431
+ * Never throws for a provider problem — the problem becomes an `error` outcome
432
+ * so a five-record run reports four successes and one preserved failure instead
433
+ * of losing all five. It DOES throw when asked to run a contact-bearing slot in
434
+ * the verification phase: that is a programming error in the caller, and
435
+ * degrading it to an outcome would let phase A quietly spend money.
436
+ */
437
+ export async function runEnrichmentSlot(input: {
438
+ slot: CrmEnrichmentSlot;
439
+ target: CrmEnrichmentTarget;
440
+ phase: CrmEnrichmentPhase;
441
+ deps?: CrmEnrichmentSlotDeps;
442
+ }): Promise<CrmEnrichmentSlotOutcome> {
443
+ const { slot, target, phase } = input;
444
+ const driver = SLOT_DRIVERS[slot];
445
+ const deps = input.deps ?? providerApiSlotDeps;
446
+
447
+ if (phase === "verify" && driver.carriesContactData) {
448
+ throw new Error(
449
+ `Slot "${slot}" carries contact data and cannot run in the verification phase.`,
450
+ );
451
+ }
452
+
453
+ let credential: { available: boolean; reason: string };
454
+ try {
455
+ credential = await deps.checkCredential({
456
+ provider: driver.provider,
457
+ key: driver.credentialKey,
458
+ });
459
+ } catch (error) {
460
+ // A credential check that itself failed is NOT "unconfigured" — we do not
461
+ // know whether the credential exists.
462
+ return { slot, status: "error", error: messageOf(error) };
463
+ }
464
+ if (!credential.available) {
465
+ return { slot, status: "unconfigured", reason: credential.reason };
466
+ }
467
+
468
+ const request = driver.buildRequest(target);
469
+ if (!request) {
470
+ return {
471
+ slot,
472
+ status: "skipped",
473
+ reason: `Record ${target.recordId} has none of the identifiers this slot resolves against.`,
474
+ };
475
+ }
476
+
477
+ try {
478
+ const response = await deps.execute(request);
479
+ const facts = extractSlotFacts(slot, response);
480
+ const observedAt = new Date().toISOString();
481
+ return facts.length === 0
482
+ ? { slot, status: "empty", observedAt }
483
+ : { slot, status: "ok", facts, observedAt };
484
+ } catch (error) {
485
+ return { slot, status: "error", error: messageOf(error) };
486
+ }
487
+ }
488
+
489
+ /** Run a set of slots for one record, concurrently. */
490
+ export async function runEnrichmentSlots(input: {
491
+ slots: readonly CrmEnrichmentSlot[];
492
+ target: CrmEnrichmentTarget;
493
+ phase: CrmEnrichmentPhase;
494
+ deps?: CrmEnrichmentSlotDeps;
495
+ }): Promise<CrmEnrichmentSlotOutcome[]> {
496
+ return Promise.all(
497
+ input.slots.map((slot) =>
498
+ runEnrichmentSlot({
499
+ slot,
500
+ target: input.target,
501
+ phase: input.phase,
502
+ deps: input.deps,
503
+ }),
504
+ ),
505
+ );
506
+ }
507
+
508
+ /** True when a record produced at least one usable fact worth paying to enrich. */
509
+ export function hasVerifiedEvidence(
510
+ outcomes: readonly CrmEnrichmentSlotOutcome[],
511
+ ): boolean {
512
+ return outcomes.some(
513
+ (outcome) => outcome.status === "ok" && outcome.facts.length > 0,
514
+ );
515
+ }
516
+
517
+ function messageOf(error: unknown): string {
518
+ return error instanceof Error ? error.message : String(error);
519
+ }
@@ -0,0 +1,65 @@
1
+ export interface DefaultDetectorDefinition {
2
+ id: string;
3
+ name: string;
4
+ description: string;
5
+ kind: "keyword" | "smart";
6
+ keywords?: string[];
7
+ classifierPrompt?: string;
8
+ enabled: true;
9
+ }
10
+
11
+ export const DEFAULT_CRM_DETECTORS: DefaultDetectorDefinition[] = [
12
+ {
13
+ id: "pricing",
14
+ name: "Pricing",
15
+ description: "Price, budget, quote, cost, or discount discussion.",
16
+ kind: "keyword",
17
+ keywords: ["price", "pricing", "quote", "cost", "discount"],
18
+ enabled: true,
19
+ },
20
+ {
21
+ id: "budget",
22
+ name: "Budget",
23
+ description: "Budget availability, approval, or constraint is discussed.",
24
+ kind: "keyword",
25
+ keywords: ["budget", "approved funds", "procurement"],
26
+ enabled: true,
27
+ },
28
+ {
29
+ id: "competitors",
30
+ name: "Competitors",
31
+ description: "A competing product, vendor, or current tool is mentioned.",
32
+ kind: "smart",
33
+ classifierPrompt:
34
+ "Match a named competitor or a clear reference to another evaluated or current solution.",
35
+ enabled: true,
36
+ },
37
+ {
38
+ id: "objections",
39
+ name: "Objections",
40
+ description: "A prospect raises concern, hesitation, or pushback.",
41
+ kind: "smart",
42
+ classifierPrompt:
43
+ "Match concerns about fit, timing, price, security, implementation, or team buy-in.",
44
+ enabled: true,
45
+ },
46
+ {
47
+ id: "next-steps",
48
+ name: "Next steps",
49
+ description:
50
+ "A concrete future action, meeting, deliverable, or follow-up.",
51
+ kind: "smart",
52
+ classifierPrompt:
53
+ "Match explicit commitments such as sending a proposal, booking a meeting, or looping in a stakeholder.",
54
+ enabled: true,
55
+ },
56
+ {
57
+ id: "timing",
58
+ name: "Timing",
59
+ description: "A deadline, renewal, launch, or urgency signal.",
60
+ kind: "smart",
61
+ classifierPrompt:
62
+ "Match a concrete timeline, deadline, renewal, quarter, or urgency signal.",
63
+ enabled: true,
64
+ },
65
+ ];
@@ -0,0 +1,105 @@
1
+ import { isSafeCrmEvidenceExcerpt } from "../../crm/crm-field-firewall.js";
2
+
3
+ export const MAX_EVIDENCE_EXCERPTS = 100;
4
+ export const MAX_EVIDENCE_QUOTE_LENGTH = 1_200;
5
+
6
+ export interface CallEvidenceExcerpt {
7
+ evidenceRef: string;
8
+ quote: string;
9
+ speaker?: string;
10
+ startSeconds: number;
11
+ endSeconds?: number;
12
+ }
13
+
14
+ const FORBIDDEN_PAYLOAD_KEYS =
15
+ /(?:audio|base64|binary|blob|body|file|image|media|payload|recording|transcript|video)/i;
16
+
17
+ function isBoundedTime(value: unknown): value is number {
18
+ return (
19
+ typeof value === "number" &&
20
+ Number.isFinite(value) &&
21
+ value >= 0 &&
22
+ value <= 86_400
23
+ );
24
+ }
25
+
26
+ function isOpaqueEvidenceRef(value: unknown): value is string {
27
+ return (
28
+ typeof value === "string" &&
29
+ value.trim().length > 0 &&
30
+ value.length <= 256 &&
31
+ !value.trimStart().startsWith("data:") &&
32
+ !/\b(?:base64|binary|blob|media)\b/i.test(value)
33
+ );
34
+ }
35
+
36
+ /**
37
+ * Narrows a Clips evidence artifact to the small, SQL-safe shape signals may use.
38
+ * Unknown payload-like keys fail closed so callers cannot accidentally pass a transcript.
39
+ */
40
+ export function parseCallEvidenceExcerpt(
41
+ value: unknown,
42
+ ): CallEvidenceExcerpt | null {
43
+ if (!value || typeof value !== "object" || Array.isArray(value)) return null;
44
+ const raw = value as Record<string, unknown>;
45
+ if (Object.keys(raw).some((key) => FORBIDDEN_PAYLOAD_KEYS.test(key))) {
46
+ return null;
47
+ }
48
+
49
+ const evidenceRef = raw.evidenceRef;
50
+ const quote = raw.quote;
51
+ const speaker = raw.speaker;
52
+ const startSeconds = raw.startSeconds;
53
+ const endSeconds = raw.endSeconds;
54
+ if (
55
+ !isOpaqueEvidenceRef(evidenceRef) ||
56
+ typeof quote !== "string" ||
57
+ quote.trim().length === 0 ||
58
+ quote.length > MAX_EVIDENCE_QUOTE_LENGTH ||
59
+ !isSafeCrmEvidenceExcerpt(quote) ||
60
+ !isBoundedTime(startSeconds) ||
61
+ (endSeconds !== undefined && !isBoundedTime(endSeconds)) ||
62
+ (typeof endSeconds === "number" && endSeconds < startSeconds) ||
63
+ (speaker !== undefined &&
64
+ (typeof speaker !== "string" ||
65
+ speaker.length > 240 ||
66
+ !isSafeCrmEvidenceExcerpt(speaker)))
67
+ ) {
68
+ return null;
69
+ }
70
+
71
+ return {
72
+ evidenceRef: evidenceRef.trim(),
73
+ quote: quote.trim(),
74
+ ...(typeof speaker === "string" && speaker.trim()
75
+ ? { speaker: speaker.trim() }
76
+ : {}),
77
+ startSeconds,
78
+ ...(typeof endSeconds === "number" ? { endSeconds } : {}),
79
+ };
80
+ }
81
+
82
+ export function parseCallEvidenceExcerpts(
83
+ values: unknown,
84
+ maxItems = MAX_EVIDENCE_EXCERPTS,
85
+ ): CallEvidenceExcerpt[] | null {
86
+ if (!Array.isArray(values) || values.length > maxItems || maxItems < 1) {
87
+ return null;
88
+ }
89
+ const excerpts: CallEvidenceExcerpt[] = [];
90
+ for (const value of values) {
91
+ const excerpt = parseCallEvidenceExcerpt(value);
92
+ if (!excerpt) return null;
93
+ excerpts.push(excerpt);
94
+ }
95
+ return excerpts;
96
+ }
97
+
98
+ export function formatEvidenceTimestamp(seconds: number): string {
99
+ const rounded = Math.max(0, Math.round(seconds));
100
+ const minutes = Math.floor(rounded / 60);
101
+ const remainder = rounded % 60;
102
+ return `${minutes.toString().padStart(2, "0")}:${remainder
103
+ .toString()
104
+ .padStart(2, "0")}`;
105
+ }