@agent-native/core 0.118.0 → 0.118.1

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 (375) hide show
  1. package/corpus/README.md +2 -2
  2. package/corpus/core/CHANGELOG.md +21 -0
  3. package/corpus/core/package.json +1 -1
  4. package/corpus/core/src/a2a/client.ts +32 -10
  5. package/corpus/core/src/cli/index.ts +18 -10
  6. package/corpus/core/src/cli/multi-frontier-runs.ts +5 -3
  7. package/corpus/core/src/cli/templates-meta.ts +0 -12
  8. package/corpus/core/src/cli/workspace-dev.ts +1 -1
  9. package/corpus/core/src/client/AgentPanel.tsx +9 -1
  10. package/corpus/core/src/client/RunStuckBanner.tsx +39 -47
  11. package/corpus/core/src/client/chat/action-chat-ui-surface.tsx +32 -0
  12. package/corpus/core/src/client/chat/message-components.tsx +53 -8
  13. package/corpus/core/src/client/chat/tool-call-display.tsx +24 -9
  14. package/corpus/core/src/client/chat/widgets/InlineExtensionWidget.tsx +0 -1
  15. package/corpus/core/src/client/chat/widgets/builtin-tool-renderers.tsx +7 -3
  16. package/corpus/core/src/client/conversation/AgentConversation.tsx +12 -1
  17. package/corpus/core/src/client/error-capture.ts +2 -6
  18. package/corpus/core/src/client/resources/McpConnectionSuggestion.tsx +20 -14
  19. package/corpus/core/src/client/resources/ResourcesPanel.tsx +5 -1
  20. package/corpus/core/src/client/resources/mcp-integration-catalog.ts +7 -17
  21. package/corpus/core/src/client/resources/mcp-integration-logos.ts +2 -2
  22. package/corpus/core/src/client/route-chunk-recovery.ts +5 -3
  23. package/corpus/core/src/client/session-replay.ts +3 -3
  24. package/corpus/core/src/client/settings/SettingsPanel.tsx +15 -6
  25. package/corpus/core/src/client/transcription/use-live-transcription.ts +64 -45
  26. package/corpus/core/src/deploy/build.ts +2 -1
  27. package/corpus/core/src/guards/db-tool-scoping.ts +1 -1
  28. package/corpus/core/src/integrations/adapters/slack.ts +84 -5
  29. package/corpus/core/src/integrations/index.ts +5 -1
  30. package/corpus/core/src/localization/default-messages.ts +1 -1
  31. package/corpus/core/src/mcp-client/oauth-routes.ts +45 -12
  32. package/corpus/core/src/server/realtime-voice.ts +4 -1
  33. package/corpus/core/src/templates/workspace-core/.agents/skills/voice-transcription/SKILL.md +7 -5
  34. package/corpus/core/src/terminal/cli-registry.ts +1 -1
  35. package/corpus/core/src/vite/client.ts +166 -1
  36. package/corpus/templates/analytics/app/hooks/use-dashboard-views.ts +13 -1
  37. package/corpus/templates/analytics/app/pages/adhoc/sql-dashboard/ViewsMenu.tsx +10 -0
  38. package/corpus/templates/analytics/changelog/2026-07-22-show-a-useful-error-when-saving-a-dashboard-view-fails.md +6 -0
  39. package/corpus/templates/brain/app/routes/sources.tsx +181 -114
  40. package/corpus/templates/clips/AGENTS.md +8 -6
  41. package/corpus/templates/clips/actions/request-transcript.ts +90 -420
  42. package/corpus/templates/clips/actions/save-browser-transcript.ts +11 -28
  43. package/corpus/templates/clips/app/components/capture-install-options.tsx +5 -3
  44. package/corpus/templates/clips/app/components/player/transcript-panel.tsx +3 -136
  45. package/corpus/templates/clips/app/lib/capture-install-options.ts +15 -4
  46. package/corpus/templates/clips/app/routes/download.tsx +3 -2
  47. package/corpus/templates/clips/changelog/2026-07-22-fixed-an-intermittent-hydration-error-on-the-download-page.md +6 -0
  48. package/corpus/templates/clips/changelog/2026-07-22-fixed-transcript-generation-when-native-speech-capture-retur.md +6 -0
  49. package/corpus/templates/clips/changelog/2026-07-22-transcript-generation-now-retries-with-backup-speech-recogni.md +6 -0
  50. package/corpus/templates/clips/server/lib/public-agent-context.ts +2 -2
  51. package/corpus/templates/clips/server/register-secrets.ts +7 -6
  52. package/corpus/templates/content/app/components/editor/SlashCommandMenu.tsx +16 -84
  53. package/corpus/templates/content/app/components/editor/VisualEditor.tsx +13 -7
  54. package/corpus/templates/content/app/components/editor/extensions/ImageBlock.tsx +2 -2
  55. package/corpus/templates/content/app/i18n-data.ts +10 -0
  56. package/corpus/templates/design/server/lib/fig-file-decoder.ts +1 -0
  57. package/corpus/templates/plan/app/entry.client.tsx +3 -0
  58. package/corpus/templates/plan/changelog/2026-07-22-recover-stale-pages-after-a-deployment-refresh-automatically.md +6 -0
  59. package/corpus/templates/slides/app/hooks/use-agent-generating.ts +10 -3
  60. package/corpus/templates/slides/app/pages/Index.tsx +1 -0
  61. package/corpus/templates/slides/changelog/2026-07-22-new-deck-prompts-now-open-in-their-own-visible-chat-thread.md +6 -0
  62. package/corpus/toolkit/CHANGELOG.md +9 -0
  63. package/corpus/toolkit/package.json +1 -1
  64. package/corpus/toolkit/src/composer/PromptComposer.tsx +5 -1
  65. package/corpus/toolkit/src/composer/RealtimeVoiceMode.tsx +119 -31
  66. package/corpus/toolkit/src/composer/useRealtimeVoiceMode.tsx +30 -0
  67. package/corpus/toolkit/src/conformance/runner.tsx +153 -1
  68. package/corpus/toolkit/src/editor/SharedRichEditor.tsx +13 -2
  69. package/corpus/toolkit/src/editor/SlashCommandMenu.tsx +20 -0
  70. package/corpus/toolkit/src/editor/index.ts +2 -0
  71. package/corpus/toolkit/src/styles.css +52 -23
  72. package/dist/a2a/client.d.ts +3 -1
  73. package/dist/a2a/client.d.ts.map +1 -1
  74. package/dist/a2a/client.js +17 -12
  75. package/dist/a2a/client.js.map +1 -1
  76. package/dist/cli/index.js +18 -8
  77. package/dist/cli/index.js.map +1 -1
  78. package/dist/cli/multi-frontier-runs.js +3 -3
  79. package/dist/cli/multi-frontier-runs.js.map +1 -1
  80. package/dist/cli/templates-meta.d.ts.map +1 -1
  81. package/dist/cli/templates-meta.js +0 -12
  82. package/dist/cli/templates-meta.js.map +1 -1
  83. package/dist/cli/workspace-dev.js +1 -1
  84. package/dist/cli/workspace-dev.js.map +1 -1
  85. package/dist/client/AgentPanel.d.ts.map +1 -1
  86. package/dist/client/AgentPanel.js +4 -1
  87. package/dist/client/AgentPanel.js.map +1 -1
  88. package/dist/client/RunStuckBanner.d.ts +3 -5
  89. package/dist/client/RunStuckBanner.d.ts.map +1 -1
  90. package/dist/client/RunStuckBanner.js +17 -18
  91. package/dist/client/RunStuckBanner.js.map +1 -1
  92. package/dist/client/chat/action-chat-ui-surface.d.ts +8 -0
  93. package/dist/client/chat/action-chat-ui-surface.d.ts.map +1 -0
  94. package/dist/client/chat/action-chat-ui-surface.js +10 -0
  95. package/dist/client/chat/action-chat-ui-surface.js.map +1 -0
  96. package/dist/client/chat/message-components.d.ts +3 -0
  97. package/dist/client/chat/message-components.d.ts.map +1 -1
  98. package/dist/client/chat/message-components.js +40 -7
  99. package/dist/client/chat/message-components.js.map +1 -1
  100. package/dist/client/chat/tool-call-display.d.ts +3 -1
  101. package/dist/client/chat/tool-call-display.d.ts.map +1 -1
  102. package/dist/client/chat/tool-call-display.js +13 -9
  103. package/dist/client/chat/tool-call-display.js.map +1 -1
  104. package/dist/client/chat/widgets/InlineExtensionWidget.d.ts.map +1 -1
  105. package/dist/client/chat/widgets/InlineExtensionWidget.js +1 -1
  106. package/dist/client/chat/widgets/InlineExtensionWidget.js.map +1 -1
  107. package/dist/client/chat/widgets/builtin-tool-renderers.d.ts.map +1 -1
  108. package/dist/client/chat/widgets/builtin-tool-renderers.js +5 -3
  109. package/dist/client/chat/widgets/builtin-tool-renderers.js.map +1 -1
  110. package/dist/client/conversation/AgentConversation.d.ts.map +1 -1
  111. package/dist/client/conversation/AgentConversation.js +3 -2
  112. package/dist/client/conversation/AgentConversation.js.map +1 -1
  113. package/dist/client/error-capture.d.ts.map +1 -1
  114. package/dist/client/error-capture.js +2 -2
  115. package/dist/client/error-capture.js.map +1 -1
  116. package/dist/client/resources/McpConnectionSuggestion.d.ts +1 -0
  117. package/dist/client/resources/McpConnectionSuggestion.d.ts.map +1 -1
  118. package/dist/client/resources/McpConnectionSuggestion.js +9 -8
  119. package/dist/client/resources/McpConnectionSuggestion.js.map +1 -1
  120. package/dist/client/resources/ResourcesPanel.d.ts.map +1 -1
  121. package/dist/client/resources/ResourcesPanel.js +3 -1
  122. package/dist/client/resources/ResourcesPanel.js.map +1 -1
  123. package/dist/client/resources/mcp-integration-catalog.d.ts +1 -0
  124. package/dist/client/resources/mcp-integration-catalog.d.ts.map +1 -1
  125. package/dist/client/resources/mcp-integration-catalog.js +4 -17
  126. package/dist/client/resources/mcp-integration-catalog.js.map +1 -1
  127. package/dist/client/resources/mcp-integration-logos.js +2 -2
  128. package/dist/client/resources/mcp-integration-logos.js.map +1 -1
  129. package/dist/client/route-chunk-recovery.d.ts.map +1 -1
  130. package/dist/client/route-chunk-recovery.js +5 -3
  131. package/dist/client/route-chunk-recovery.js.map +1 -1
  132. package/dist/client/session-replay.js +3 -3
  133. package/dist/client/session-replay.js.map +1 -1
  134. package/dist/client/settings/SettingsPanel.d.ts.map +1 -1
  135. package/dist/client/settings/SettingsPanel.js +2 -2
  136. package/dist/client/settings/SettingsPanel.js.map +1 -1
  137. package/dist/client/transcription/use-live-transcription.d.ts +3 -3
  138. package/dist/client/transcription/use-live-transcription.d.ts.map +1 -1
  139. package/dist/client/transcription/use-live-transcription.js +22 -11
  140. package/dist/client/transcription/use-live-transcription.js.map +1 -1
  141. package/dist/collab/routes.d.ts +1 -1
  142. package/dist/collab/struct-routes.d.ts +1 -1
  143. package/dist/deploy/build.d.ts.map +1 -1
  144. package/dist/deploy/build.js +1 -1
  145. package/dist/deploy/build.js.map +1 -1
  146. package/dist/guards/db-tool-scoping.js +1 -1
  147. package/dist/guards/db-tool-scoping.js.map +1 -1
  148. package/dist/integrations/adapters/slack.d.ts +1 -0
  149. package/dist/integrations/adapters/slack.d.ts.map +1 -1
  150. package/dist/integrations/adapters/slack.js +61 -5
  151. package/dist/integrations/adapters/slack.js.map +1 -1
  152. package/dist/integrations/index.d.ts +1 -1
  153. package/dist/integrations/index.d.ts.map +1 -1
  154. package/dist/integrations/index.js +1 -1
  155. package/dist/integrations/index.js.map +1 -1
  156. package/dist/localization/default-messages.js +1 -1
  157. package/dist/localization/default-messages.js.map +1 -1
  158. package/dist/mcp-client/oauth-routes.d.ts +3 -0
  159. package/dist/mcp-client/oauth-routes.d.ts.map +1 -1
  160. package/dist/mcp-client/oauth-routes.js +41 -12
  161. package/dist/mcp-client/oauth-routes.js.map +1 -1
  162. package/dist/notifications/routes.d.ts +2 -2
  163. package/dist/observability/routes.d.ts +3 -3
  164. package/dist/provider-api/actions/custom-provider-registration.d.ts +8 -8
  165. package/dist/provider-api/actions/provider-api.d.ts +11 -11
  166. package/dist/secrets/routes.d.ts +6 -6
  167. package/dist/server/realtime-voice.d.ts.map +1 -1
  168. package/dist/server/realtime-voice.js +1 -1
  169. package/dist/server/realtime-voice.js.map +1 -1
  170. package/dist/server/transcribe-voice.d.ts +1 -1
  171. package/dist/templates/workspace-core/.agents/skills/voice-transcription/SKILL.md +7 -5
  172. package/dist/terminal/cli-registry.js +1 -1
  173. package/dist/terminal/cli-registry.js.map +1 -1
  174. package/dist/vite/client.d.ts +8 -1
  175. package/dist/vite/client.d.ts.map +1 -1
  176. package/dist/vite/client.js +113 -2
  177. package/dist/vite/client.js.map +1 -1
  178. package/package.json +2 -2
  179. package/src/a2a/client.ts +32 -10
  180. package/src/cli/index.ts +18 -10
  181. package/src/cli/multi-frontier-runs.ts +5 -3
  182. package/src/cli/templates-meta.ts +0 -12
  183. package/src/cli/workspace-dev.ts +1 -1
  184. package/src/client/AgentPanel.tsx +9 -1
  185. package/src/client/RunStuckBanner.tsx +39 -47
  186. package/src/client/chat/action-chat-ui-surface.tsx +32 -0
  187. package/src/client/chat/message-components.tsx +53 -8
  188. package/src/client/chat/tool-call-display.tsx +24 -9
  189. package/src/client/chat/widgets/InlineExtensionWidget.tsx +0 -1
  190. package/src/client/chat/widgets/builtin-tool-renderers.tsx +7 -3
  191. package/src/client/conversation/AgentConversation.tsx +12 -1
  192. package/src/client/error-capture.ts +2 -6
  193. package/src/client/resources/McpConnectionSuggestion.tsx +20 -14
  194. package/src/client/resources/ResourcesPanel.tsx +5 -1
  195. package/src/client/resources/mcp-integration-catalog.ts +7 -17
  196. package/src/client/resources/mcp-integration-logos.ts +2 -2
  197. package/src/client/route-chunk-recovery.ts +5 -3
  198. package/src/client/session-replay.ts +3 -3
  199. package/src/client/settings/SettingsPanel.tsx +15 -6
  200. package/src/client/transcription/use-live-transcription.ts +64 -45
  201. package/src/deploy/build.ts +2 -1
  202. package/src/guards/db-tool-scoping.ts +1 -1
  203. package/src/integrations/adapters/slack.ts +84 -5
  204. package/src/integrations/index.ts +5 -1
  205. package/src/localization/default-messages.ts +1 -1
  206. package/src/mcp-client/oauth-routes.ts +45 -12
  207. package/src/server/realtime-voice.ts +4 -1
  208. package/src/templates/workspace-core/.agents/skills/voice-transcription/SKILL.md +7 -5
  209. package/src/terminal/cli-registry.ts +1 -1
  210. package/src/vite/client.ts +166 -1
  211. package/corpus/templates/crm/.agents/skills/agent-native-docs/SKILL.md +0 -115
  212. package/corpus/templates/crm/.agents/skills/agent-native-toolkit/SKILL.md +0 -177
  213. package/corpus/templates/crm/.agents/skills/crm/SKILL.md +0 -128
  214. package/corpus/templates/crm/.agents/skills/customizing-agent-native/SKILL.md +0 -220
  215. package/corpus/templates/crm/.agents/skills/feature-flags/SKILL.md +0 -169
  216. package/corpus/templates/crm/.agents/skills/upgrade-agent-native/SKILL.md +0 -100
  217. package/corpus/templates/crm/.env.example +0 -5
  218. package/corpus/templates/crm/AGENTS.md +0 -120
  219. package/corpus/templates/crm/CHANGELOG.md +0 -1
  220. package/corpus/templates/crm/DEVELOPING.md +0 -66
  221. package/corpus/templates/crm/README.md +0 -106
  222. package/corpus/templates/crm/_gitignore +0 -12
  223. package/corpus/templates/crm/actions/_crm-action-utils.ts +0 -87
  224. package/corpus/templates/crm/actions/_crm-dashboard.ts +0 -69
  225. package/corpus/templates/crm/actions/_crm-data-program-actions.ts +0 -15
  226. package/corpus/templates/crm/actions/_crm-signal-utils.ts +0 -79
  227. package/corpus/templates/crm/actions/apply-crm-proposals.ts +0 -203
  228. package/corpus/templates/crm/actions/attach-call-evidence.ts +0 -170
  229. package/corpus/templates/crm/actions/configure-crm-connection.ts +0 -126
  230. package/corpus/templates/crm/actions/configure-native-crm.ts +0 -44
  231. package/corpus/templates/crm/actions/create-crm-record.ts +0 -148
  232. package/corpus/templates/crm/actions/create-crm-signal-tracker.ts +0 -58
  233. package/corpus/templates/crm/actions/delete-staged-dataset.ts +0 -14
  234. package/corpus/templates/crm/actions/get-crm-automation-recipe.ts +0 -36
  235. package/corpus/templates/crm/actions/get-crm-dashboard-panel.ts +0 -35
  236. package/corpus/templates/crm/actions/get-crm-dashboard.ts +0 -14
  237. package/corpus/templates/crm/actions/get-crm-overview.ts +0 -13
  238. package/corpus/templates/crm/actions/get-crm-pipeline-data.ts +0 -61
  239. package/corpus/templates/crm/actions/get-crm-record.ts +0 -75
  240. package/corpus/templates/crm/actions/install-crm-pipeline-dashboard.ts +0 -81
  241. package/corpus/templates/crm/actions/list-crm-dashboard-revisions.ts +0 -15
  242. package/corpus/templates/crm/actions/list-crm-dashboards.ts +0 -15
  243. package/corpus/templates/crm/actions/list-crm-proposals.ts +0 -31
  244. package/corpus/templates/crm/actions/list-crm-records.ts +0 -79
  245. package/corpus/templates/crm/actions/list-crm-saved-views.ts +0 -15
  246. package/corpus/templates/crm/actions/list-crm-signal-hits.ts +0 -43
  247. package/corpus/templates/crm/actions/list-crm-signal-trackers.ts +0 -32
  248. package/corpus/templates/crm/actions/list-crm-tasks.ts +0 -22
  249. package/corpus/templates/crm/actions/list-staged-datasets.ts +0 -14
  250. package/corpus/templates/crm/actions/list-workspace-connections.ts +0 -47
  251. package/corpus/templates/crm/actions/manage-crm-signal-tracker.ts +0 -87
  252. package/corpus/templates/crm/actions/manage-crm-task.ts +0 -132
  253. package/corpus/templates/crm/actions/navigate.ts +0 -63
  254. package/corpus/templates/crm/actions/provider-api-catalog.ts +0 -22
  255. package/corpus/templates/crm/actions/provider-api-docs.ts +0 -54
  256. package/corpus/templates/crm/actions/provider-api-request.ts +0 -92
  257. package/corpus/templates/crm/actions/query-staged-dataset.ts +0 -44
  258. package/corpus/templates/crm/actions/record-crm-call-insight.ts +0 -160
  259. package/corpus/templates/crm/actions/record-crm-smart-signal.ts +0 -150
  260. package/corpus/templates/crm/actions/restore-crm-dashboard-revision.ts +0 -23
  261. package/corpus/templates/crm/actions/review-crm-signal.ts +0 -31
  262. package/corpus/templates/crm/actions/run-crm-saved-view-program.ts +0 -103
  263. package/corpus/templates/crm/actions/run-crm-signal-trackers.ts +0 -275
  264. package/corpus/templates/crm/actions/run.ts +0 -7
  265. package/corpus/templates/crm/actions/save-crm-dashboard.ts +0 -51
  266. package/corpus/templates/crm/actions/save-crm-saved-view.ts +0 -119
  267. package/corpus/templates/crm/actions/sync-crm.ts +0 -146
  268. package/corpus/templates/crm/actions/update-crm-record.ts +0 -510
  269. package/corpus/templates/crm/actions/view-screen.ts +0 -218
  270. package/corpus/templates/crm/agent-native.app-skill.json +0 -58
  271. package/corpus/templates/crm/app/components/crm/CreateCrmRecordDialog.tsx +0 -158
  272. package/corpus/templates/crm/app/components/crm/CrmDashboardPanel.tsx +0 -78
  273. package/corpus/templates/crm/app/components/crm/CrmSignalsPanel.tsx +0 -226
  274. package/corpus/templates/crm/app/components/crm/IntelligenceSettings.tsx +0 -414
  275. package/corpus/templates/crm/app/components/crm/RecordActions.tsx +0 -510
  276. package/corpus/templates/crm/app/components/crm/RecordGrid.tsx +0 -134
  277. package/corpus/templates/crm/app/components/crm/RecordWorkspace.tsx +0 -362
  278. package/corpus/templates/crm/app/components/crm/SavedViewDataProgram.tsx +0 -93
  279. package/corpus/templates/crm/app/components/crm/Surface.tsx +0 -97
  280. package/corpus/templates/crm/app/components/crm/WorkOverview.tsx +0 -180
  281. package/corpus/templates/crm/app/components/layout/CrmLayout.tsx +0 -112
  282. package/corpus/templates/crm/app/components/layout/CrmSidebar.tsx +0 -84
  283. package/corpus/templates/crm/app/components/ui/alert-dialog.tsx +0 -1
  284. package/corpus/templates/crm/app/components/ui/badge.tsx +0 -1
  285. package/corpus/templates/crm/app/components/ui/button.tsx +0 -1
  286. package/corpus/templates/crm/app/components/ui/card.tsx +0 -1
  287. package/corpus/templates/crm/app/components/ui/dialog.tsx +0 -1
  288. package/corpus/templates/crm/app/components/ui/input.tsx +0 -1
  289. package/corpus/templates/crm/app/components/ui/label.tsx +0 -1
  290. package/corpus/templates/crm/app/components/ui/select.tsx +0 -1
  291. package/corpus/templates/crm/app/components/ui/sheet.tsx +0 -1
  292. package/corpus/templates/crm/app/components/ui/skeleton.tsx +0 -1
  293. package/corpus/templates/crm/app/components/ui/switch.tsx +0 -1
  294. package/corpus/templates/crm/app/components/ui/table.tsx +0 -1
  295. package/corpus/templates/crm/app/components/ui/tabs.tsx +0 -1
  296. package/corpus/templates/crm/app/components/ui/textarea.tsx +0 -1
  297. package/corpus/templates/crm/app/components/ui/toolkit-provider.tsx +0 -17
  298. package/corpus/templates/crm/app/design-system.ts +0 -3
  299. package/corpus/templates/crm/app/entry.client.tsx +0 -16
  300. package/corpus/templates/crm/app/entry.server.tsx +0 -10
  301. package/corpus/templates/crm/app/global.css +0 -167
  302. package/corpus/templates/crm/app/hooks/use-navigation-state.ts +0 -60
  303. package/corpus/templates/crm/app/i18n/en-US.ts +0 -89
  304. package/corpus/templates/crm/app/i18n/index.ts +0 -34
  305. package/corpus/templates/crm/app/lib/dashboard.ts +0 -13
  306. package/corpus/templates/crm/app/lib/navigation.ts +0 -54
  307. package/corpus/templates/crm/app/lib/tab-id.ts +0 -1
  308. package/corpus/templates/crm/app/lib/types.ts +0 -172
  309. package/corpus/templates/crm/app/lib/utils.ts +0 -1
  310. package/corpus/templates/crm/app/root.tsx +0 -91
  311. package/corpus/templates/crm/app/routes/_index.tsx +0 -26
  312. package/corpus/templates/crm/app/routes/accounts.tsx +0 -41
  313. package/corpus/templates/crm/app/routes/agent.tsx +0 -5
  314. package/corpus/templates/crm/app/routes/ask.tsx +0 -53
  315. package/corpus/templates/crm/app/routes/dashboard.tsx +0 -118
  316. package/corpus/templates/crm/app/routes/opportunities.tsx +0 -41
  317. package/corpus/templates/crm/app/routes/people.tsx +0 -41
  318. package/corpus/templates/crm/app/routes/proposals.tsx +0 -266
  319. package/corpus/templates/crm/app/routes/records.$recordId.tsx +0 -35
  320. package/corpus/templates/crm/app/routes/settings.tsx +0 -63
  321. package/corpus/templates/crm/app/routes/setup.tsx +0 -308
  322. package/corpus/templates/crm/app/routes/tasks.tsx +0 -252
  323. package/corpus/templates/crm/app/routes/views.tsx +0 -333
  324. package/corpus/templates/crm/app/routes.ts +0 -17
  325. package/corpus/templates/crm/app/vite-env.d.ts +0 -6
  326. package/corpus/templates/crm/changelog/2026-07-21-agent-native-crm.md +0 -6
  327. package/corpus/templates/crm/changelog/2026-07-21-clips-call-evidence-review-recipe.md +0 -6
  328. package/corpus/templates/crm/changelog/2026-07-21-crm-can-now-find-and-review-evidence-grounded-call-signals-w.md +0 -6
  329. package/corpus/templates/crm/changelog/2026-07-21-crm-can-now-manage-keyword-and-smart-call-signal-trackers-fr.md +0 -6
  330. package/corpus/templates/crm/changelog/2026-07-21-delegated-local-crm-automation.md +0 -6
  331. package/corpus/templates/crm/changelog/2026-07-21-native-sql.md +0 -6
  332. package/corpus/templates/crm/changelog/2026-07-21-pipeline-dashboard.md +0 -6
  333. package/corpus/templates/crm/changelog/2026-07-21-pipeline-dashboards-now-install-correctly-from-the-crm-actio.md +0 -6
  334. package/corpus/templates/crm/changelog/2026-07-21-salesforce-connection.md +0 -6
  335. package/corpus/templates/crm/changelog/2026-07-22-crm-field-validation-now-explains-which-fields-cannot-be-edi.md +0 -6
  336. package/corpus/templates/crm/changelog/2026-07-22-fixed-hubspot-contact-syncs-missing-records-updated-after-th.md +0 -6
  337. package/corpus/templates/crm/changelog/2026-07-22-fixed-pipeline-dashboard-panels-so-their-opportunity-data-lo.md +0 -6
  338. package/corpus/templates/crm/changelog/2026-07-22-full-page-chat-keeps-the-active-conversation-when-moving-to-.md +0 -6
  339. package/corpus/templates/crm/components.json +0 -20
  340. package/corpus/templates/crm/docs/architecture/crm-contract.md +0 -180
  341. package/corpus/templates/crm/learnings.defaults.md +0 -5
  342. package/corpus/templates/crm/package.json +0 -59
  343. package/corpus/templates/crm/public/favicon.svg +0 -1
  344. package/corpus/templates/crm/public/icon-180.svg +0 -1
  345. package/corpus/templates/crm/public/manifest.json +0 -11
  346. package/corpus/templates/crm/react-router.config.ts +0 -7
  347. package/corpus/templates/crm/server/crm/adapter.ts +0 -32
  348. package/corpus/templates/crm/server/crm/crm-field-firewall.ts +0 -64
  349. package/corpus/templates/crm/server/crm/crm-mirror.ts +0 -711
  350. package/corpus/templates/crm/server/crm/hubspot-adapter.ts +0 -1146
  351. package/corpus/templates/crm/server/crm/native-adapter.ts +0 -1715
  352. package/corpus/templates/crm/server/crm/read-through.ts +0 -207
  353. package/corpus/templates/crm/server/crm/salesforce-adapter.ts +0 -1255
  354. package/corpus/templates/crm/server/db/crm-store.ts +0 -1170
  355. package/corpus/templates/crm/server/db/index.ts +0 -169
  356. package/corpus/templates/crm/server/db/schema.ts +0 -441
  357. package/corpus/templates/crm/server/lib/intelligence/default-detectors.ts +0 -65
  358. package/corpus/templates/crm/server/lib/intelligence/evidence.ts +0 -105
  359. package/corpus/templates/crm/server/lib/intelligence/keyword-detector.ts +0 -100
  360. package/corpus/templates/crm/server/lib/intelligence/smart-detector.ts +0 -144
  361. package/corpus/templates/crm/server/lib/intelligence/summary.ts +0 -141
  362. package/corpus/templates/crm/server/lib/provider-api.ts +0 -64
  363. package/corpus/templates/crm/server/middleware/auth.ts +0 -4
  364. package/corpus/templates/crm/server/plugins/agent-chat.ts +0 -68
  365. package/corpus/templates/crm/server/plugins/auth.ts +0 -14
  366. package/corpus/templates/crm/server/plugins/core-routes.ts +0 -17
  367. package/corpus/templates/crm/server/plugins/db.ts +0 -454
  368. package/corpus/templates/crm/server/routes/[...page].get.ts +0 -5
  369. package/corpus/templates/crm/shared/crm-automation-recipes.ts +0 -132
  370. package/corpus/templates/crm/shared/crm-contract.ts +0 -254
  371. package/corpus/templates/crm/shared/crm-sales-config.ts +0 -63
  372. package/corpus/templates/crm/ssr-entry.ts +0 -11
  373. package/corpus/templates/crm/tsconfig.json +0 -24
  374. package/corpus/templates/crm/vite.config.ts +0 -15
  375. package/corpus/templates/crm/vitest.config.ts +0 -5
@@ -1,1715 +0,0 @@
1
- import { accessFilter, assertAccess } from "@agent-native/core/sharing";
2
- import { and, asc, eq, gt, inArray, isNull, like, or } from "drizzle-orm";
3
-
4
- import type {
5
- CrmAccessScope,
6
- CrmAdapter,
7
- CrmAdapterCapabilities,
8
- CrmConnectionRef,
9
- CrmFieldDefinition,
10
- CrmMutation,
11
- CrmMutationResult,
12
- CrmObjectDefinition,
13
- CrmObjectKind,
14
- CrmRecord,
15
- CrmRecordRef,
16
- CrmRelationship,
17
- CrmSyncScope,
18
- CrmSyncPage,
19
- CrmValue,
20
- } from "../../shared/crm-contract.js";
21
- import { getDb, schema } from "../db/index.js";
22
- import {
23
- isBoundedCrmValue,
24
- isSafeCrmMutationFieldName,
25
- } from "./crm-field-firewall.js";
26
-
27
- const MAX_PAGE_SIZE = 100;
28
- const MAX_FIELDS = 80;
29
- const MAX_MUTATION_FIELDS = 20;
30
- const STANDARD_OBJECTS = ["accounts", "people", "opportunities"] as const;
31
-
32
- type Ownership = {
33
- ownerEmail: string;
34
- orgId: string | null;
35
- visibility: "private" | "org" | "public";
36
- };
37
-
38
- type NativeConnection = Ownership & {
39
- id: string;
40
- accountId: string | null;
41
- accessScopeKey: string;
42
- accessScopeJson: string;
43
- };
44
-
45
- type StoredField = {
46
- fieldName: string;
47
- valueType: string;
48
- stringValue: string | null;
49
- numberValue: number | null;
50
- booleanValue: boolean | null;
51
- jsonValue: string | null;
52
- };
53
-
54
- function kindForObject(objectType: string): CrmObjectKind {
55
- if (objectType === "accounts") return "account";
56
- if (objectType === "people") return "person";
57
- if (objectType === "opportunities") return "opportunity";
58
- return "custom";
59
- }
60
-
61
- function labelForObject(objectType: string): string {
62
- if (objectType === "accounts") return "Account";
63
- if (objectType === "people") return "Person";
64
- if (objectType === "opportunities") return "Opportunity";
65
- return objectType
66
- .replace(/[_-]+/g, " ")
67
- .replace(/\b\w/g, (character) => character.toUpperCase());
68
- }
69
-
70
- function nativeField(
71
- name: string,
72
- valueType: CrmFieldDefinition["valueType"],
73
- required = false,
74
- ): CrmFieldDefinition {
75
- return {
76
- name,
77
- label: name
78
- .replace(/([A-Z])/g, " $1")
79
- .replace(/^./, (character) => character.toUpperCase()),
80
- valueType,
81
- storagePolicy: "local-authoritative",
82
- sensitive: false,
83
- readable: true,
84
- createable: true,
85
- updateable: true,
86
- required,
87
- };
88
- }
89
-
90
- export function nativeObjectTemplate(objectType: string): CrmObjectDefinition {
91
- const objectFields =
92
- objectType === "accounts"
93
- ? [
94
- nativeField("name", "string", true),
95
- nativeField("domain", "string"),
96
- nativeField("industry", "string"),
97
- nativeField("ownerName", "string"),
98
- ]
99
- : objectType === "people"
100
- ? [
101
- nativeField("firstName", "string"),
102
- nativeField("lastName", "string"),
103
- nativeField("email", "string"),
104
- nativeField("title", "string"),
105
- nativeField("accountId", "reference"),
106
- nativeField("ownerName", "string"),
107
- ]
108
- : objectType === "opportunities"
109
- ? [
110
- nativeField("name", "string", true),
111
- nativeField("amount", "currency"),
112
- nativeField("stage", "string"),
113
- nativeField("closeDate", "date"),
114
- nativeField("accountId", "reference"),
115
- nativeField("ownerName", "string"),
116
- ]
117
- : [];
118
- const fields = objectFields.length
119
- ? [
120
- ...objectFields,
121
- nativeField("desiredCadenceDays", "number"),
122
- nativeField("lastMeaningfulInteractionAt", "datetime"),
123
- nativeField("nextContactAt", "datetime"),
124
- ]
125
- : [];
126
- const label = labelForObject(objectType);
127
- return {
128
- connectionId: "",
129
- provider: "native",
130
- objectType,
131
- kind: kindForObject(objectType),
132
- label,
133
- pluralLabel: `${label}s`,
134
- custom: !STANDARD_OBJECTS.includes(
135
- objectType as (typeof STANDARD_OBJECTS)[number],
136
- ),
137
- queryable: true,
138
- searchable: true,
139
- createable: true,
140
- updateable: true,
141
- deleteable: true,
142
- fields,
143
- };
144
- }
145
-
146
- function valueType(value: CrmValue): CrmFieldDefinition["valueType"] {
147
- if (typeof value === "boolean") return "boolean";
148
- if (typeof value === "number") return "number";
149
- if (Array.isArray(value) || typeof value === "object") return "json";
150
- return "string";
151
- }
152
-
153
- function fieldColumns(value: CrmValue) {
154
- if (typeof value === "string") {
155
- return {
156
- stringValue: value,
157
- numberValue: null,
158
- booleanValue: null,
159
- jsonValue: null,
160
- };
161
- }
162
- if (typeof value === "number") {
163
- return {
164
- stringValue: null,
165
- numberValue: value,
166
- booleanValue: null,
167
- jsonValue: null,
168
- };
169
- }
170
- if (typeof value === "boolean") {
171
- return {
172
- stringValue: null,
173
- numberValue: null,
174
- booleanValue: value,
175
- jsonValue: null,
176
- };
177
- }
178
- if (value === null) {
179
- return {
180
- stringValue: null,
181
- numberValue: null,
182
- booleanValue: null,
183
- jsonValue: null,
184
- };
185
- }
186
- return {
187
- stringValue: null,
188
- numberValue: null,
189
- booleanValue: null,
190
- jsonValue: JSON.stringify(value),
191
- };
192
- }
193
-
194
- function fieldValue(field: StoredField): CrmValue {
195
- if (field.stringValue !== null) return field.stringValue;
196
- if (field.numberValue !== null) return field.numberValue;
197
- if (field.booleanValue !== null) return field.booleanValue;
198
- if (!field.jsonValue) return null;
199
- try {
200
- const value = JSON.parse(field.jsonValue) as unknown;
201
- return isBoundedCrmValue(value) ? (value as CrmValue) : null;
202
- } catch {
203
- return null;
204
- }
205
- }
206
-
207
- function encodeCursor(offset: number): string {
208
- return btoa(String(offset)).replace(/=+$/, "");
209
- }
210
-
211
- function decodeCursor(cursor: string | undefined): number {
212
- if (!cursor) return 0;
213
- try {
214
- const value = Number.parseInt(atob(cursor), 10);
215
- return Number.isSafeInteger(value) && value >= 0 ? value : 0;
216
- } catch {
217
- return 0;
218
- }
219
- }
220
-
221
- export function nextNativeRevision(value: string | null | undefined): string {
222
- try {
223
- const parsed = BigInt(value ?? "0");
224
- return (parsed >= 0n ? parsed + 1n : 1n).toString();
225
- } catch {
226
- return "1";
227
- }
228
- }
229
-
230
- function scopeFromConnection(connection: NativeConnection): CrmAccessScope {
231
- try {
232
- const stored = JSON.parse(
233
- connection.accessScopeJson,
234
- ) as Partial<CrmAccessScope>;
235
- if (stored.mode === "native" && stored.key === connection.accessScopeKey) {
236
- return {
237
- key: stored.key,
238
- actorId: stored.actorId ?? connection.ownerEmail,
239
- mode: "native",
240
- objectReadable: true,
241
- objectCreateable: true,
242
- objectUpdateable: true,
243
- objectDeleteable: true,
244
- recordVisibility:
245
- stored.recordVisibility ??
246
- (connection.visibility === "org" ? "workspace" : "actor"),
247
- ...(stored.fieldPermissionsHash
248
- ? { fieldPermissionsHash: stored.fieldPermissionsHash }
249
- : {}),
250
- ...(stored.sharingFingerprint
251
- ? { sharingFingerprint: stored.sharingFingerprint }
252
- : {}),
253
- };
254
- }
255
- } catch {}
256
- return {
257
- key: connection.accessScopeKey,
258
- actorId: connection.ownerEmail,
259
- mode: "native",
260
- objectReadable: true,
261
- objectCreateable: true,
262
- objectUpdateable: true,
263
- objectDeleteable: true,
264
- recordVisibility: connection.visibility === "org" ? "workspace" : "actor",
265
- };
266
- }
267
-
268
- function json(value: unknown, maximum = 12_000): string {
269
- const encoded = JSON.stringify(value);
270
- if (encoded.length > maximum)
271
- throw new Error("CRM mutation payload exceeds the allowed size.");
272
- return encoded;
273
- }
274
-
275
- function mutationFields(mutation: CrmMutation): Record<string, CrmValue> {
276
- const fields = mutation.fields ?? {};
277
- const entries = Object.entries(fields);
278
- if (
279
- entries.length > MAX_MUTATION_FIELDS ||
280
- entries.some(
281
- ([name, value]) =>
282
- !name ||
283
- name.length > 120 ||
284
- !isSafeCrmMutationFieldName(name) ||
285
- !isBoundedCrmValue(value),
286
- )
287
- ) {
288
- throw new Error(
289
- "Native CRM mutations require bounded, non-media field values.",
290
- );
291
- }
292
- return fields;
293
- }
294
-
295
- function summaryColumns(fields: Record<string, CrmValue>, fallback: string) {
296
- const string = (...names: string[]) => {
297
- for (const name of names) {
298
- const value = fields[name];
299
- if (typeof value === "string") return value.slice(0, 500);
300
- }
301
- return null;
302
- };
303
- const number = (...names: string[]) => {
304
- for (const name of names) {
305
- const value = fields[name];
306
- if (typeof value === "number") return value;
307
- }
308
- return null;
309
- };
310
- const desiredCadenceDays = fields.desiredCadenceDays;
311
- if (
312
- desiredCadenceDays !== undefined &&
313
- desiredCadenceDays !== null &&
314
- (typeof desiredCadenceDays !== "number" ||
315
- !Number.isInteger(desiredCadenceDays) ||
316
- desiredCadenceDays < 1 ||
317
- desiredCadenceDays > 365)
318
- ) {
319
- throw new Error(
320
- "desiredCadenceDays must be null or an integer from 1 to 365.",
321
- );
322
- }
323
- return {
324
- displayName: string("displayName", "name", "Name") ?? fallback,
325
- primaryEmail: string("email", "Email"),
326
- domain: string("domain", "website", "Website"),
327
- stage: string("stage", "StageName"),
328
- ownerName: string("ownerName"),
329
- amount: number("amount", "Amount"),
330
- closeDate: string("closeDate", "CloseDate"),
331
- desiredCadenceDays: desiredCadenceDays ?? null,
332
- lastMeaningfulInteractionAt: string("lastMeaningfulInteractionAt"),
333
- nextContactAt: string("nextContactAt"),
334
- };
335
- }
336
-
337
- async function loadNativeConnection(
338
- connectionId: string,
339
- accessTier: "viewer" | "editor" = "viewer",
340
- ): Promise<NativeConnection> {
341
- await assertAccess("crm-connection", connectionId, accessTier);
342
- const db = getDb();
343
- const [connection] = await db
344
- .select({
345
- id: schema.crmConnections.id,
346
- accountId: schema.crmConnections.accountId,
347
- accessScopeKey: schema.crmConnections.accessScopeKey,
348
- accessScopeJson: schema.crmConnections.accessScopeJson,
349
- ownerEmail: schema.crmConnections.ownerEmail,
350
- orgId: schema.crmConnections.orgId,
351
- visibility: schema.crmConnections.visibility,
352
- })
353
- .from(schema.crmConnections)
354
- .where(
355
- and(
356
- eq(schema.crmConnections.id, connectionId),
357
- eq(schema.crmConnections.provider, "native"),
358
- accessFilter(
359
- schema.crmConnections,
360
- schema.crmConnectionShares,
361
- undefined,
362
- accessTier,
363
- ),
364
- ),
365
- )
366
- .limit(1);
367
- if (!connection) throw new Error("Native CRM connection is unavailable.");
368
- return connection as NativeConnection;
369
- }
370
-
371
- async function ensureNativeObject(input: {
372
- connection: NativeConnection;
373
- objectType: string;
374
- fields: CrmFieldDefinition[];
375
- }) {
376
- const db = getDb();
377
- const template = nativeObjectTemplate(input.objectType);
378
- const object = { ...template, connectionId: input.connection.id };
379
- const [existing] = await db
380
- .select({ id: schema.crmObjects.id })
381
- .from(schema.crmObjects)
382
- .where(
383
- and(
384
- eq(schema.crmObjects.connectionId, input.connection.id),
385
- eq(schema.crmObjects.objectType, input.objectType),
386
- accessFilter(schema.crmObjects, schema.crmObjectShares),
387
- ),
388
- )
389
- .limit(1);
390
- const now = new Date().toISOString();
391
- const objectValues = {
392
- provider: "native" as const,
393
- objectType: input.objectType,
394
- kind: object.kind,
395
- label: object.label,
396
- pluralLabel: object.pluralLabel,
397
- custom: object.custom,
398
- queryable: true,
399
- searchable: true,
400
- createable: true,
401
- updateable: true,
402
- deleteable: true,
403
- capabilitiesJson: json(NATIVE_CAPABILITIES, 4_000),
404
- updatedAt: now,
405
- };
406
- if (existing) {
407
- await assertAccess("crm-object", existing.id, "editor");
408
- await db
409
- .update(schema.crmObjects)
410
- .set(objectValues)
411
- .where(
412
- and(
413
- eq(schema.crmObjects.id, existing.id),
414
- accessFilter(
415
- schema.crmObjects,
416
- schema.crmObjectShares,
417
- undefined,
418
- "editor",
419
- ),
420
- ),
421
- );
422
- } else {
423
- await db.insert(schema.crmObjects).values({
424
- id: crypto.randomUUID(),
425
- connectionId: input.connection.id,
426
- ...objectValues,
427
- ownerEmail: input.connection.ownerEmail,
428
- orgId: input.connection.orgId,
429
- visibility: input.connection.visibility,
430
- createdAt: now,
431
- });
432
- }
433
- const known = new Map<string, CrmFieldDefinition>();
434
- for (const field of [...template.fields, ...input.fields])
435
- known.set(field.name, field);
436
- const existingPolicies = await db
437
- .select({
438
- id: schema.crmFieldPolicies.id,
439
- fieldName: schema.crmFieldPolicies.fieldName,
440
- })
441
- .from(schema.crmFieldPolicies)
442
- .where(
443
- and(
444
- eq(schema.crmFieldPolicies.connectionId, input.connection.id),
445
- eq(schema.crmFieldPolicies.objectType, input.objectType),
446
- accessFilter(schema.crmFieldPolicies, schema.crmFieldPolicyShares),
447
- ),
448
- );
449
- const policyByName = new Map(
450
- existingPolicies.map((policy) => [policy.fieldName, policy]),
451
- );
452
- for (const field of known.values()) {
453
- const existingPolicy = policyByName.get(field.name);
454
- const values = {
455
- label: field.label,
456
- valueType: field.valueType,
457
- storagePolicy: "local-authoritative" as const,
458
- sensitive: false,
459
- readable: true,
460
- createable: true,
461
- updateable: true,
462
- required: field.required,
463
- metadataJson: "{}",
464
- updatedAt: now,
465
- };
466
- if (existingPolicy) {
467
- await assertAccess("crm-field-policy", existingPolicy.id, "editor");
468
- await db
469
- .update(schema.crmFieldPolicies)
470
- .set(values)
471
- .where(
472
- and(
473
- eq(schema.crmFieldPolicies.id, existingPolicy.id),
474
- accessFilter(
475
- schema.crmFieldPolicies,
476
- schema.crmFieldPolicyShares,
477
- undefined,
478
- "editor",
479
- ),
480
- ),
481
- );
482
- } else {
483
- await db.insert(schema.crmFieldPolicies).values({
484
- id: crypto.randomUUID(),
485
- connectionId: input.connection.id,
486
- objectType: input.objectType,
487
- fieldName: field.name,
488
- ...values,
489
- ownerEmail: input.connection.ownerEmail,
490
- orgId: input.connection.orgId,
491
- visibility: input.connection.visibility,
492
- createdAt: now,
493
- });
494
- }
495
- }
496
- }
497
-
498
- export async function initializeNativeCrmDomain(connectionId: string) {
499
- const connection = await loadNativeConnection(connectionId, "editor");
500
- await Promise.all(
501
- STANDARD_OBJECTS.map((objectType) =>
502
- ensureNativeObject({ connection, objectType, fields: [] }),
503
- ),
504
- );
505
- }
506
-
507
- export async function configureNativeCrmConnection(input: {
508
- label?: string;
509
- ownership: Ownership;
510
- }) {
511
- const db = getDb();
512
- const label = input.label?.trim() || "Native SQL";
513
- const [existing] = await db
514
- .select({ id: schema.crmConnections.id })
515
- .from(schema.crmConnections)
516
- .where(
517
- and(
518
- eq(schema.crmConnections.provider, "native"),
519
- eq(schema.crmConnections.label, label),
520
- accessFilter(schema.crmConnections, schema.crmConnectionShares),
521
- ),
522
- )
523
- .limit(1);
524
- const id = existing?.id ?? crypto.randomUUID();
525
- const now = new Date().toISOString();
526
- const accessScope: CrmAccessScope = {
527
- key: `native:${id}`,
528
- actorId: input.ownership.ownerEmail,
529
- mode: "native",
530
- objectReadable: true,
531
- objectCreateable: true,
532
- objectUpdateable: true,
533
- objectDeleteable: true,
534
- recordVisibility:
535
- input.ownership.visibility === "org" ? "workspace" : "actor",
536
- };
537
- const values = {
538
- provider: "native" as const,
539
- workspaceConnectionId: null,
540
- label,
541
- accountId: null,
542
- mode: "native" as const,
543
- status: "connected" as const,
544
- selectedPipelinesJson: "[]",
545
- selectedObjectTypesJson: json(STANDARD_OBJECTS, 1_000),
546
- accessScopeKey: accessScope.key,
547
- accessScopeJson: json(accessScope, 4_000),
548
- lastError: null,
549
- updatedAt: now,
550
- };
551
- if (existing) {
552
- await assertAccess("crm-connection", existing.id, "editor");
553
- await db
554
- .update(schema.crmConnections)
555
- .set(values)
556
- .where(
557
- and(
558
- eq(schema.crmConnections.id, existing.id),
559
- accessFilter(
560
- schema.crmConnections,
561
- schema.crmConnectionShares,
562
- undefined,
563
- "editor",
564
- ),
565
- ),
566
- );
567
- } else {
568
- await db.insert(schema.crmConnections).values({
569
- id,
570
- ...values,
571
- ownerEmail: input.ownership.ownerEmail,
572
- orgId: input.ownership.orgId,
573
- visibility: input.ownership.visibility,
574
- createdAt: now,
575
- });
576
- }
577
- await initializeNativeCrmDomain(id);
578
- return { id, label, accessScope, ...input.ownership };
579
- }
580
-
581
- export async function resolveNativeCrmAccessScope(input: {
582
- connectionId: string;
583
- objectType: string;
584
- }): Promise<CrmAccessScope | null> {
585
- const connection = await loadNativeConnection(input.connectionId);
586
- const db = getDb();
587
- const [object] = await db
588
- .select({ id: schema.crmObjects.id })
589
- .from(schema.crmObjects)
590
- .where(
591
- and(
592
- eq(schema.crmObjects.connectionId, connection.id),
593
- eq(schema.crmObjects.objectType, input.objectType),
594
- accessFilter(schema.crmObjects, schema.crmObjectShares),
595
- ),
596
- )
597
- .limit(1);
598
- return object ? scopeFromConnection(connection) : null;
599
- }
600
-
601
- async function fieldsForRecord(recordId: string, requested?: string[]) {
602
- const db = getDb();
603
- const rows = await db
604
- .select({
605
- fieldName: schema.crmRecordFields.fieldName,
606
- valueType: schema.crmRecordFields.valueType,
607
- stringValue: schema.crmRecordFields.stringValue,
608
- numberValue: schema.crmRecordFields.numberValue,
609
- booleanValue: schema.crmRecordFields.booleanValue,
610
- jsonValue: schema.crmRecordFields.jsonValue,
611
- })
612
- .from(schema.crmRecordFields)
613
- .where(
614
- and(
615
- eq(schema.crmRecordFields.recordId, recordId),
616
- eq(schema.crmRecordFields.storagePolicy, "local-authoritative"),
617
- accessFilter(schema.crmRecordFields, schema.crmRecordFieldShares),
618
- ),
619
- );
620
- const allowed = requested?.length ? new Set(requested) : null;
621
- return Object.fromEntries(
622
- rows
623
- .filter((field) => !allowed || allowed.has(field.fieldName))
624
- .map((field) => [field.fieldName, fieldValue(field as StoredField)]),
625
- ) as Record<string, CrmValue>;
626
- }
627
-
628
- async function materializeRecord(input: {
629
- row: typeof schema.crmRecords.$inferSelect;
630
- connection: NativeConnection;
631
- fields?: string[];
632
- }): Promise<CrmRecord> {
633
- const fields = await fieldsForRecord(input.row.id, input.fields);
634
- const scope = scopeFromConnection(input.connection);
635
- const observedAt = input.row.updatedAt;
636
- return {
637
- ref: {
638
- connectionId: input.connection.id,
639
- provider: "native",
640
- accountId: input.connection.accountId ?? undefined,
641
- actorId: scope.actorId,
642
- objectType: input.row.objectType,
643
- kind: input.row.kind as CrmObjectKind,
644
- remoteId: input.row.remoteId,
645
- localId: input.row.id,
646
- },
647
- displayName: input.row.displayName,
648
- fields,
649
- remoteRevision: input.row.remoteRevision ?? undefined,
650
- remoteUpdatedAt: input.row.remoteUpdatedAt ?? undefined,
651
- deleted: input.row.tombstone,
652
- accessScope: scope,
653
- provenance: Object.keys(fields).map((fieldName) => ({
654
- provider: "native",
655
- connectionId: input.connection.id,
656
- objectType: input.row.objectType,
657
- remoteId: input.row.remoteId,
658
- fieldName,
659
- remoteRevision: input.row.remoteRevision ?? undefined,
660
- observedAt,
661
- })),
662
- };
663
- }
664
-
665
- const NATIVE_CAPABILITIES: CrmAdapterCapabilities = {
666
- schemaDiscovery: true,
667
- customObjects: true,
668
- search: true,
669
- incrementalSync: true,
670
- deletedRecordSync: true,
671
- conditionalMutations: true,
672
- labeledRelationships: true,
673
- perFieldPermissions: true,
674
- perRecordPermissions: true,
675
- };
676
-
677
- export class NativeCrmAdapter implements CrmAdapter {
678
- readonly capabilities = NATIVE_CAPABILITIES;
679
- readonly connection: CrmConnectionRef;
680
- readonly #nativeConnection: NativeConnection;
681
- readonly #initiatedBy: "human" | "agent" | "automation";
682
-
683
- constructor(
684
- connection: NativeConnection,
685
- initiatedBy: "human" | "agent" | "automation" = "agent",
686
- ) {
687
- this.#nativeConnection = connection;
688
- this.#initiatedBy = initiatedBy;
689
- const scope = scopeFromConnection(connection);
690
- this.connection = {
691
- connectionId: connection.id,
692
- provider: "native",
693
- ...(connection.accountId ? { accountId: connection.accountId } : {}),
694
- ...(scope.actorId ? { actorId: scope.actorId } : {}),
695
- };
696
- }
697
-
698
- getAccessScope(): CrmAccessScope {
699
- return scopeFromConnection(this.#nativeConnection);
700
- }
701
-
702
- async discoverObjects(): Promise<CrmObjectDefinition[]> {
703
- const db = getDb();
704
- const objects = await db
705
- .select()
706
- .from(schema.crmObjects)
707
- .where(
708
- and(
709
- eq(schema.crmObjects.connectionId, this.connection.connectionId),
710
- eq(schema.crmObjects.provider, "native"),
711
- accessFilter(schema.crmObjects, schema.crmObjectShares),
712
- ),
713
- )
714
- .orderBy(asc(schema.crmObjects.objectType));
715
- return Promise.all(
716
- objects.map((object) => this.describeObject(object.objectType)),
717
- );
718
- }
719
-
720
- async describeObject(objectType: string): Promise<CrmObjectDefinition> {
721
- const db = getDb();
722
- const [object] = await db
723
- .select()
724
- .from(schema.crmObjects)
725
- .where(
726
- and(
727
- eq(schema.crmObjects.connectionId, this.connection.connectionId),
728
- eq(schema.crmObjects.objectType, objectType),
729
- accessFilter(schema.crmObjects, schema.crmObjectShares),
730
- ),
731
- )
732
- .limit(1);
733
- if (!object)
734
- throw new Error(`Native CRM object ${objectType} is not registered.`);
735
- const policies = await db
736
- .select()
737
- .from(schema.crmFieldPolicies)
738
- .where(
739
- and(
740
- eq(
741
- schema.crmFieldPolicies.connectionId,
742
- this.connection.connectionId,
743
- ),
744
- eq(schema.crmFieldPolicies.objectType, objectType),
745
- accessFilter(schema.crmFieldPolicies, schema.crmFieldPolicyShares),
746
- ),
747
- )
748
- .orderBy(asc(schema.crmFieldPolicies.fieldName));
749
- return {
750
- ...this.connection,
751
- objectType,
752
- kind: object.kind as CrmObjectKind,
753
- label: object.label,
754
- pluralLabel: object.pluralLabel,
755
- custom: object.custom,
756
- queryable: object.queryable,
757
- searchable: object.searchable,
758
- createable: object.createable,
759
- updateable: object.updateable,
760
- deleteable: object.deleteable,
761
- fields: policies.map((policy) => ({
762
- name: policy.fieldName,
763
- label: policy.label,
764
- valueType: policy.valueType as CrmFieldDefinition["valueType"],
765
- storagePolicy: "local-authoritative",
766
- sensitive: policy.sensitive,
767
- readable: policy.readable,
768
- createable: policy.createable,
769
- updateable: policy.updateable,
770
- required: policy.required,
771
- })),
772
- };
773
- }
774
-
775
- async syncPage(input: {
776
- scope: CrmSyncScope;
777
- fieldAllowList: string[];
778
- cursor?: string;
779
- limit: number;
780
- }): Promise<CrmSyncPage> {
781
- const db = getDb();
782
- const limit = Math.min(Math.max(input.limit, 1), MAX_PAGE_SIZE);
783
- const offset = decodeCursor(input.cursor);
784
- const conditions = [
785
- eq(schema.crmRecords.connectionId, this.connection.connectionId),
786
- eq(schema.crmRecords.provider, "native"),
787
- eq(schema.crmRecords.objectType, input.scope.objectType),
788
- accessFilter(schema.crmRecords, schema.crmRecordShares),
789
- ];
790
- if (!input.scope.includeDeleted)
791
- conditions.push(eq(schema.crmRecords.tombstone, false));
792
- if (input.scope.recordIds?.length)
793
- conditions.push(
794
- inArray(schema.crmRecords.remoteId, input.scope.recordIds),
795
- );
796
- if (input.scope.ownerIds?.length)
797
- conditions.push(
798
- inArray(schema.crmRecords.ownerRemoteId, input.scope.ownerIds),
799
- );
800
- if (input.scope.pipelineIds?.length)
801
- conditions.push(
802
- inArray(schema.crmRecords.pipelineId, input.scope.pipelineIds),
803
- );
804
- if (input.scope.updatedAfter)
805
- conditions.push(
806
- gt(schema.crmRecords.updatedAt, input.scope.updatedAfter),
807
- );
808
- if (input.scope.associatedRecordIds?.length) {
809
- throw new Error(
810
- "Native CRM sync does not infer associated-record cohorts; use explicit recordIds.",
811
- );
812
- }
813
- const rows = await db
814
- .select()
815
- .from(schema.crmRecords)
816
- .where(and(...conditions))
817
- .orderBy(asc(schema.crmRecords.updatedAt), asc(schema.crmRecords.id))
818
- .limit(limit + 1)
819
- .offset(offset);
820
- const page = rows.slice(0, limit);
821
- return {
822
- records: await Promise.all(
823
- page.map((row) =>
824
- materializeRecord({
825
- row,
826
- connection: this.#nativeConnection,
827
- fields: input.fieldAllowList.slice(0, MAX_FIELDS),
828
- }),
829
- ),
830
- ),
831
- relationships: [],
832
- ...(rows.length > limit
833
- ? { nextCursor: encodeCursor(offset + limit) }
834
- : {}),
835
- complete: rows.length <= limit,
836
- };
837
- }
838
-
839
- async getRecord(input: {
840
- record: CrmRecordRef;
841
- fields: string[];
842
- }): Promise<CrmRecord | null> {
843
- const db = getDb();
844
- const conditions = [
845
- eq(schema.crmRecords.connectionId, this.connection.connectionId),
846
- eq(schema.crmRecords.provider, "native"),
847
- eq(schema.crmRecords.objectType, input.record.objectType),
848
- eq(schema.crmRecords.remoteId, input.record.remoteId),
849
- accessFilter(schema.crmRecords, schema.crmRecordShares),
850
- ];
851
- if (input.record.localId)
852
- conditions.push(eq(schema.crmRecords.id, input.record.localId));
853
- const [row] = await db
854
- .select()
855
- .from(schema.crmRecords)
856
- .where(and(...conditions))
857
- .limit(1);
858
- return row
859
- ? materializeRecord({
860
- row,
861
- connection: this.#nativeConnection,
862
- fields: input.fields.slice(0, MAX_FIELDS),
863
- })
864
- : null;
865
- }
866
-
867
- async search(input: {
868
- objectTypes: string[];
869
- query: string;
870
- fields: string[];
871
- limit: number;
872
- cursor?: string;
873
- }): Promise<CrmSyncPage> {
874
- if (!input.objectTypes.length || input.objectTypes.length > 50)
875
- throw new Error("Native CRM search requires 1 to 50 object types.");
876
- const db = getDb();
877
- const limit = Math.min(Math.max(input.limit, 1), MAX_PAGE_SIZE);
878
- const offset = decodeCursor(input.cursor);
879
- const query = input.query.trim().slice(0, 120);
880
- const rows = await db
881
- .select()
882
- .from(schema.crmRecords)
883
- .where(
884
- and(
885
- eq(schema.crmRecords.connectionId, this.connection.connectionId),
886
- eq(schema.crmRecords.provider, "native"),
887
- eq(schema.crmRecords.tombstone, false),
888
- inArray(schema.crmRecords.objectType, input.objectTypes),
889
- or(
890
- like(schema.crmRecords.displayName, `%${query}%`),
891
- like(schema.crmRecords.primaryEmail, `%${query}%`),
892
- like(schema.crmRecords.domain, `%${query}%`),
893
- ),
894
- accessFilter(schema.crmRecords, schema.crmRecordShares),
895
- ),
896
- )
897
- .orderBy(asc(schema.crmRecords.displayName), asc(schema.crmRecords.id))
898
- .limit(limit + 1)
899
- .offset(offset);
900
- const page = rows.slice(0, limit);
901
- return {
902
- records: await Promise.all(
903
- page.map((row) =>
904
- materializeRecord({
905
- row,
906
- connection: this.#nativeConnection,
907
- fields: input.fields.slice(0, MAX_FIELDS),
908
- }),
909
- ),
910
- ),
911
- relationships: [],
912
- ...(rows.length > limit
913
- ? { nextCursor: encodeCursor(offset + limit) }
914
- : {}),
915
- complete: rows.length <= limit,
916
- };
917
- }
918
-
919
- async listRelationships(input: {
920
- record: CrmRecordRef;
921
- targetObjectTypes?: string[];
922
- limit: number;
923
- cursor?: string;
924
- }): Promise<{
925
- relationships: CrmRelationship[];
926
- nextCursor?: string;
927
- complete: boolean;
928
- }> {
929
- const source = await this.getRecord({ record: input.record, fields: [] });
930
- if (!source?.ref.localId) return { relationships: [], complete: true };
931
- const db = getDb();
932
- const limit = Math.min(Math.max(input.limit, 1), MAX_PAGE_SIZE);
933
- const rows = await db
934
- .select()
935
- .from(schema.crmRelationships)
936
- .where(
937
- and(
938
- eq(
939
- schema.crmRelationships.connectionId,
940
- this.connection.connectionId,
941
- ),
942
- eq(schema.crmRelationships.fromRecordId, source.ref.localId),
943
- eq(schema.crmRelationships.tombstone, false),
944
- accessFilter(schema.crmRelationships, schema.crmRelationshipShares),
945
- ),
946
- )
947
- .orderBy(asc(schema.crmRelationships.id))
948
- .limit(limit + 1)
949
- .offset(decodeCursor(input.cursor));
950
- const page = rows.slice(0, limit);
951
- const targetIds = page.map((row) => row.toRecordId);
952
- if (!targetIds.length)
953
- return { relationships: [], complete: rows.length <= limit };
954
- const targets = await db
955
- .select()
956
- .from(schema.crmRecords)
957
- .where(
958
- and(
959
- inArray(schema.crmRecords.id, targetIds),
960
- eq(schema.crmRecords.connectionId, this.connection.connectionId),
961
- eq(schema.crmRecords.tombstone, false),
962
- accessFilter(schema.crmRecords, schema.crmRecordShares),
963
- ),
964
- );
965
- const byId = new Map(targets.map((target) => [target.id, target]));
966
- const relationships = page.flatMap((relationship) => {
967
- const target = byId.get(relationship.toRecordId);
968
- if (
969
- !target ||
970
- (input.targetObjectTypes?.length &&
971
- !input.targetObjectTypes.includes(target.objectType))
972
- )
973
- return [];
974
- return [
975
- {
976
- from: source.ref,
977
- to: {
978
- ...this.connection,
979
- objectType: target.objectType,
980
- kind: target.kind as CrmObjectKind,
981
- remoteId: target.remoteId,
982
- localId: target.id,
983
- },
984
- relationshipType: relationship.relationshipType,
985
- ...(relationship.label ? { label: relationship.label } : {}),
986
- ...(relationship.inverseLabel
987
- ? { inverseLabel: relationship.inverseLabel }
988
- : {}),
989
- ...(relationship.sourceField
990
- ? { sourceField: relationship.sourceField }
991
- : {}),
992
- },
993
- ];
994
- });
995
- const offset = decodeCursor(input.cursor);
996
- return {
997
- relationships,
998
- ...(rows.length > limit
999
- ? { nextCursor: encodeCursor(offset + limit) }
1000
- : {}),
1001
- complete: rows.length <= limit,
1002
- };
1003
- }
1004
-
1005
- async applyMutation(mutation: CrmMutation): Promise<CrmMutationResult> {
1006
- if (
1007
- mutation.record.connectionId !== this.connection.connectionId ||
1008
- mutation.record.provider !== "native"
1009
- ) {
1010
- return {
1011
- status: "rejected",
1012
- message: "Native CRM mutations must target this native connection.",
1013
- };
1014
- }
1015
- if (!mutation.idempotencyKey || mutation.idempotencyKey.length > 256) {
1016
- return {
1017
- status: "rejected",
1018
- message: "Native CRM mutations require a bounded idempotency key.",
1019
- };
1020
- }
1021
- const fields = mutationFields(mutation);
1022
- if (mutation.operation === "create")
1023
- return this.createRecord(mutation, fields);
1024
- if (mutation.operation === "update")
1025
- return this.updateRecord(mutation, fields);
1026
- if (mutation.operation === "delete") return this.deleteRecord(mutation);
1027
- if (
1028
- mutation.operation === "associate" ||
1029
- mutation.operation === "disassociate"
1030
- ) {
1031
- return this.mutateRelationship(mutation);
1032
- }
1033
- return { status: "rejected", message: "Unsupported native CRM mutation." };
1034
- }
1035
-
1036
- private async replayOrStart(mutation: CrmMutation, patchJson: string) {
1037
- const db = getDb();
1038
- const [existing] = await db
1039
- .select()
1040
- .from(schema.crmMutations)
1041
- .where(
1042
- and(
1043
- eq(schema.crmMutations.idempotencyKey, mutation.idempotencyKey),
1044
- accessFilter(schema.crmMutations, schema.crmMutationShares),
1045
- ),
1046
- )
1047
- .limit(1);
1048
- if (!existing) return null;
1049
- if (
1050
- existing.operation !== mutation.operation ||
1051
- existing.patchJson !== patchJson
1052
- ) {
1053
- throw new Error(
1054
- "Native CRM idempotency key was already used for a different mutation.",
1055
- );
1056
- }
1057
- if (existing.status === "conflict")
1058
- return {
1059
- status: "conflict" as const,
1060
- message: existing.error ?? undefined,
1061
- };
1062
- const record = existing.recordId
1063
- ? await this.getRecord({
1064
- record: { ...mutation.record, localId: existing.recordId },
1065
- fields: [],
1066
- })
1067
- : null;
1068
- return {
1069
- status: "applied" as const,
1070
- ...(record ? { record, remoteRevision: record.remoteRevision } : {}),
1071
- };
1072
- }
1073
-
1074
- private async createRecord(
1075
- mutation: CrmMutation,
1076
- fields: Record<string, CrmValue>,
1077
- ): Promise<CrmMutationResult> {
1078
- const objectType = mutation.record.objectType;
1079
- await ensureNativeObject({
1080
- connection: this.#nativeConnection,
1081
- objectType,
1082
- fields: Object.entries(fields).map(([name, value]) =>
1083
- nativeField(name, valueType(value)),
1084
- ),
1085
- });
1086
- const patchJson = json({ fields });
1087
- const replay = await this.replayOrStart(mutation, patchJson);
1088
- if (replay) return replay;
1089
- const db = getDb();
1090
- const remoteId = mutation.record.remoteId || crypto.randomUUID();
1091
- const recordId = crypto.randomUUID();
1092
- const now = new Date().toISOString();
1093
- const revision = "1";
1094
- try {
1095
- await db.transaction(async (tx) => {
1096
- const [duplicate] = await tx
1097
- .select({ id: schema.crmRecords.id })
1098
- .from(schema.crmRecords)
1099
- .where(
1100
- and(
1101
- eq(schema.crmRecords.connectionId, this.connection.connectionId),
1102
- eq(schema.crmRecords.objectType, objectType),
1103
- eq(schema.crmRecords.remoteId, remoteId),
1104
- accessFilter(schema.crmRecords, schema.crmRecordShares),
1105
- ),
1106
- )
1107
- .limit(1);
1108
- if (duplicate) throw new Error("NATIVE_CRM_DUPLICATE_RECORD");
1109
- await tx.insert(schema.crmRecords).values({
1110
- id: recordId,
1111
- connectionId: this.connection.connectionId,
1112
- provider: "native",
1113
- objectType,
1114
- kind: kindForObject(objectType),
1115
- remoteId,
1116
- ...summaryColumns(fields, remoteId),
1117
- remoteRevision: revision,
1118
- remoteUpdatedAt: now,
1119
- accessScopeKey: this.getAccessScope().key,
1120
- accessScopeJson: json(this.getAccessScope(), 4_000),
1121
- tombstone: false,
1122
- ownerEmail: this.#nativeConnection.ownerEmail,
1123
- orgId: this.#nativeConnection.orgId,
1124
- visibility: this.#nativeConnection.visibility,
1125
- createdAt: now,
1126
- updatedAt: now,
1127
- });
1128
- for (const [fieldName, value] of Object.entries(fields)) {
1129
- await tx.insert(schema.crmRecordFields).values({
1130
- id: crypto.randomUUID(),
1131
- recordId,
1132
- fieldName,
1133
- valueType: valueType(value),
1134
- storagePolicy: "local-authoritative",
1135
- ...fieldColumns(value),
1136
- provenanceJson: json(
1137
- [
1138
- {
1139
- provider: "native",
1140
- connectionId: this.connection.connectionId,
1141
- objectType,
1142
- remoteId,
1143
- fieldName,
1144
- remoteRevision: revision,
1145
- observedAt: now,
1146
- },
1147
- ],
1148
- 4_000,
1149
- ),
1150
- accessScopeKey: this.getAccessScope().key,
1151
- accessScopeJson: json(this.getAccessScope(), 4_000),
1152
- remoteRevision: revision,
1153
- ownerEmail: this.#nativeConnection.ownerEmail,
1154
- orgId: this.#nativeConnection.orgId,
1155
- visibility: this.#nativeConnection.visibility,
1156
- createdAt: now,
1157
- updatedAt: now,
1158
- });
1159
- }
1160
- await tx.insert(schema.crmMutations).values({
1161
- id: crypto.randomUUID(),
1162
- recordId,
1163
- connectionId: this.connection.connectionId,
1164
- operation: "create",
1165
- initiatedBy: this.#initiatedBy,
1166
- target: "local",
1167
- policyDecision: "execute",
1168
- risk: "routine",
1169
- status: "applied",
1170
- patchJson,
1171
- beforeJson: "{}",
1172
- afterJson: json({ remoteId, revision }),
1173
- idempotencyKey: mutation.idempotencyKey,
1174
- providerRemoteRevision: revision,
1175
- appliedAt: now,
1176
- ownerEmail: this.#nativeConnection.ownerEmail,
1177
- orgId: this.#nativeConnection.orgId,
1178
- visibility: this.#nativeConnection.visibility,
1179
- createdAt: now,
1180
- updatedAt: now,
1181
- });
1182
- });
1183
- } catch (error) {
1184
- if (
1185
- error instanceof Error &&
1186
- error.message === "NATIVE_CRM_DUPLICATE_RECORD"
1187
- )
1188
- return {
1189
- status: "conflict",
1190
- message: "A native CRM record with this id already exists.",
1191
- };
1192
- throw error;
1193
- }
1194
- const record = await this.getRecord({
1195
- record: { ...mutation.record, remoteId, localId: recordId },
1196
- fields: Object.keys(fields),
1197
- });
1198
- return record
1199
- ? { status: "applied", record, remoteRevision: revision }
1200
- : { status: "rejected", message: "Native CRM record was not persisted." };
1201
- }
1202
-
1203
- private async updateRecord(
1204
- mutation: CrmMutation,
1205
- fields: Record<string, CrmValue>,
1206
- ): Promise<CrmMutationResult> {
1207
- await ensureNativeObject({
1208
- connection: this.#nativeConnection,
1209
- objectType: mutation.record.objectType,
1210
- fields: Object.entries(fields).map(([name, value]) =>
1211
- nativeField(name, valueType(value)),
1212
- ),
1213
- });
1214
- const patchJson = json({ fields });
1215
- const replay = await this.replayOrStart(mutation, patchJson);
1216
- if (replay) return replay;
1217
- const db = getDb();
1218
- const [record] = await db
1219
- .select()
1220
- .from(schema.crmRecords)
1221
- .where(
1222
- and(
1223
- eq(schema.crmRecords.connectionId, this.connection.connectionId),
1224
- eq(schema.crmRecords.objectType, mutation.record.objectType),
1225
- eq(schema.crmRecords.remoteId, mutation.record.remoteId),
1226
- accessFilter(
1227
- schema.crmRecords,
1228
- schema.crmRecordShares,
1229
- undefined,
1230
- "editor",
1231
- ),
1232
- ),
1233
- )
1234
- .limit(1);
1235
- if (!record || record.tombstone)
1236
- return {
1237
- status: "rejected",
1238
- message: "Native CRM record is unavailable.",
1239
- };
1240
- await assertAccess("crm-record", record.id, "editor");
1241
- if (
1242
- mutation.expectedRemoteRevision &&
1243
- mutation.expectedRemoteRevision !== record.remoteRevision
1244
- )
1245
- return {
1246
- status: "conflict",
1247
- message: "Native CRM record revision changed.",
1248
- };
1249
- const current = await fieldsForRecord(record.id);
1250
- const merged = { ...current, ...fields };
1251
- const revision = nextNativeRevision(record.remoteRevision);
1252
- const now = new Date().toISOString();
1253
- const revisionGuard =
1254
- record.remoteRevision === null
1255
- ? isNull(schema.crmRecords.remoteRevision)
1256
- : eq(schema.crmRecords.remoteRevision, record.remoteRevision);
1257
- try {
1258
- await db.transaction(async (tx) => {
1259
- const guarded = await tx
1260
- .update(schema.crmRecords)
1261
- .set({
1262
- ...summaryColumns(merged, record.remoteId),
1263
- remoteRevision: revision,
1264
- remoteUpdatedAt: now,
1265
- updatedAt: now,
1266
- })
1267
- .where(
1268
- and(
1269
- eq(schema.crmRecords.id, record.id),
1270
- revisionGuard,
1271
- accessFilter(
1272
- schema.crmRecords,
1273
- schema.crmRecordShares,
1274
- undefined,
1275
- "editor",
1276
- ),
1277
- ),
1278
- )
1279
- .returning({ id: schema.crmRecords.id });
1280
- if (guarded.length !== 1)
1281
- throw new Error("NATIVE_CRM_REVISION_CONFLICT");
1282
- for (const [fieldName, value] of Object.entries(fields)) {
1283
- const [existing] = await tx
1284
- .select({ id: schema.crmRecordFields.id })
1285
- .from(schema.crmRecordFields)
1286
- .where(
1287
- and(
1288
- eq(schema.crmRecordFields.recordId, record.id),
1289
- eq(schema.crmRecordFields.fieldName, fieldName),
1290
- accessFilter(
1291
- schema.crmRecordFields,
1292
- schema.crmRecordFieldShares,
1293
- undefined,
1294
- "editor",
1295
- ),
1296
- ),
1297
- )
1298
- .limit(1);
1299
- const values = {
1300
- valueType: valueType(value),
1301
- storagePolicy: "local-authoritative" as const,
1302
- ...fieldColumns(value),
1303
- provenanceJson: json(
1304
- [
1305
- {
1306
- provider: "native",
1307
- connectionId: this.connection.connectionId,
1308
- objectType: record.objectType,
1309
- remoteId: record.remoteId,
1310
- fieldName,
1311
- remoteRevision: revision,
1312
- observedAt: now,
1313
- },
1314
- ],
1315
- 4_000,
1316
- ),
1317
- accessScopeKey: this.getAccessScope().key,
1318
- accessScopeJson: json(this.getAccessScope(), 4_000),
1319
- remoteRevision: revision,
1320
- updatedAt: now,
1321
- };
1322
- if (existing) {
1323
- await tx
1324
- .update(schema.crmRecordFields)
1325
- .set(values)
1326
- .where(
1327
- and(
1328
- eq(schema.crmRecordFields.id, existing.id),
1329
- accessFilter(
1330
- schema.crmRecordFields,
1331
- schema.crmRecordFieldShares,
1332
- undefined,
1333
- "editor",
1334
- ),
1335
- ),
1336
- );
1337
- } else {
1338
- await tx.insert(schema.crmRecordFields).values({
1339
- id: crypto.randomUUID(),
1340
- recordId: record.id,
1341
- fieldName,
1342
- ...values,
1343
- ownerEmail: record.ownerEmail,
1344
- orgId: record.orgId,
1345
- visibility: record.visibility,
1346
- createdAt: now,
1347
- });
1348
- }
1349
- }
1350
- await tx.insert(schema.crmMutations).values({
1351
- id: crypto.randomUUID(),
1352
- recordId: record.id,
1353
- connectionId: this.connection.connectionId,
1354
- operation: "update",
1355
- initiatedBy: this.#initiatedBy,
1356
- target: "local",
1357
- policyDecision: "execute",
1358
- risk: "routine",
1359
- status: "applied",
1360
- patchJson,
1361
- beforeJson: json({ remoteRevision: record.remoteRevision }),
1362
- afterJson: json({ remoteRevision: revision }),
1363
- idempotencyKey: mutation.idempotencyKey,
1364
- expectedRemoteRevision: mutation.expectedRemoteRevision ?? null,
1365
- providerRemoteRevision: revision,
1366
- appliedAt: now,
1367
- ownerEmail: record.ownerEmail,
1368
- orgId: record.orgId,
1369
- visibility: record.visibility,
1370
- createdAt: now,
1371
- updatedAt: now,
1372
- });
1373
- });
1374
- } catch (error) {
1375
- if (
1376
- error instanceof Error &&
1377
- error.message === "NATIVE_CRM_REVISION_CONFLICT"
1378
- )
1379
- return {
1380
- status: "conflict",
1381
- message: "Native CRM record revision changed.",
1382
- };
1383
- throw error;
1384
- }
1385
- const updated = await this.getRecord({
1386
- record: mutation.record,
1387
- fields: Object.keys(merged),
1388
- });
1389
- return updated
1390
- ? { status: "applied", record: updated, remoteRevision: revision }
1391
- : { status: "rejected", message: "Native CRM record was not updated." };
1392
- }
1393
-
1394
- private async deleteRecord(
1395
- mutation: CrmMutation,
1396
- ): Promise<CrmMutationResult> {
1397
- const patchJson = "{}";
1398
- const replay = await this.replayOrStart(mutation, patchJson);
1399
- if (replay) return replay;
1400
- const db = getDb();
1401
- const [record] = await db
1402
- .select()
1403
- .from(schema.crmRecords)
1404
- .where(
1405
- and(
1406
- eq(schema.crmRecords.connectionId, this.connection.connectionId),
1407
- eq(schema.crmRecords.objectType, mutation.record.objectType),
1408
- eq(schema.crmRecords.remoteId, mutation.record.remoteId),
1409
- accessFilter(
1410
- schema.crmRecords,
1411
- schema.crmRecordShares,
1412
- undefined,
1413
- "editor",
1414
- ),
1415
- ),
1416
- )
1417
- .limit(1);
1418
- if (!record)
1419
- return {
1420
- status: "rejected",
1421
- message: "Native CRM record is unavailable.",
1422
- };
1423
- await assertAccess("crm-record", record.id, "editor");
1424
- if (
1425
- mutation.expectedRemoteRevision &&
1426
- mutation.expectedRemoteRevision !== record.remoteRevision
1427
- )
1428
- return {
1429
- status: "conflict",
1430
- message: "Native CRM record revision changed.",
1431
- };
1432
- const revision = nextNativeRevision(record.remoteRevision);
1433
- const now = new Date().toISOString();
1434
- const revisionGuard =
1435
- record.remoteRevision === null
1436
- ? isNull(schema.crmRecords.remoteRevision)
1437
- : eq(schema.crmRecords.remoteRevision, record.remoteRevision);
1438
- try {
1439
- await db.transaction(async (tx) => {
1440
- const guarded = await tx
1441
- .update(schema.crmRecords)
1442
- .set({
1443
- tombstone: true,
1444
- remoteRevision: revision,
1445
- remoteUpdatedAt: now,
1446
- updatedAt: now,
1447
- })
1448
- .where(
1449
- and(
1450
- eq(schema.crmRecords.id, record.id),
1451
- revisionGuard,
1452
- accessFilter(
1453
- schema.crmRecords,
1454
- schema.crmRecordShares,
1455
- undefined,
1456
- "editor",
1457
- ),
1458
- ),
1459
- )
1460
- .returning({ id: schema.crmRecords.id });
1461
- if (guarded.length !== 1)
1462
- throw new Error("NATIVE_CRM_REVISION_CONFLICT");
1463
- const relationships = await tx
1464
- .select({ id: schema.crmRelationships.id })
1465
- .from(schema.crmRelationships)
1466
- .where(
1467
- and(
1468
- eq(
1469
- schema.crmRelationships.connectionId,
1470
- this.connection.connectionId,
1471
- ),
1472
- or(
1473
- eq(schema.crmRelationships.fromRecordId, record.id),
1474
- eq(schema.crmRelationships.toRecordId, record.id),
1475
- ),
1476
- accessFilter(
1477
- schema.crmRelationships,
1478
- schema.crmRelationshipShares,
1479
- undefined,
1480
- "editor",
1481
- ),
1482
- ),
1483
- );
1484
- for (const relationship of relationships) {
1485
- await assertAccess("crm-relationship", relationship.id, "editor");
1486
- await tx
1487
- .update(schema.crmRelationships)
1488
- .set({ tombstone: true, updatedAt: now })
1489
- .where(
1490
- and(
1491
- eq(schema.crmRelationships.id, relationship.id),
1492
- accessFilter(
1493
- schema.crmRelationships,
1494
- schema.crmRelationshipShares,
1495
- undefined,
1496
- "editor",
1497
- ),
1498
- ),
1499
- );
1500
- }
1501
- await tx.insert(schema.crmMutations).values({
1502
- id: crypto.randomUUID(),
1503
- recordId: record.id,
1504
- connectionId: this.connection.connectionId,
1505
- operation: "delete",
1506
- initiatedBy: this.#initiatedBy,
1507
- target: "local",
1508
- policyDecision: "execute",
1509
- risk: "routine",
1510
- status: "applied",
1511
- patchJson,
1512
- beforeJson: json({ remoteRevision: record.remoteRevision }),
1513
- afterJson: json({ remoteRevision: revision, tombstone: true }),
1514
- idempotencyKey: mutation.idempotencyKey,
1515
- expectedRemoteRevision: mutation.expectedRemoteRevision ?? null,
1516
- providerRemoteRevision: revision,
1517
- appliedAt: now,
1518
- ownerEmail: record.ownerEmail,
1519
- orgId: record.orgId,
1520
- visibility: record.visibility,
1521
- createdAt: now,
1522
- updatedAt: now,
1523
- });
1524
- });
1525
- } catch (error) {
1526
- if (
1527
- error instanceof Error &&
1528
- error.message === "NATIVE_CRM_REVISION_CONFLICT"
1529
- )
1530
- return {
1531
- status: "conflict",
1532
- message: "Native CRM record revision changed.",
1533
- };
1534
- throw error;
1535
- }
1536
- const deleted = await this.getRecord({
1537
- record: mutation.record,
1538
- fields: [],
1539
- });
1540
- return {
1541
- status: "applied",
1542
- ...(deleted ? { record: deleted } : {}),
1543
- remoteRevision: revision,
1544
- };
1545
- }
1546
-
1547
- private async mutateRelationship(
1548
- mutation: CrmMutation,
1549
- ): Promise<CrmMutationResult> {
1550
- if (!mutation.relationship)
1551
- return {
1552
- status: "rejected",
1553
- message: "Native relationship mutations require an edge.",
1554
- };
1555
- const patchJson = json({ relationship: mutation.relationship });
1556
- const replay = await this.replayOrStart(mutation, patchJson);
1557
- if (replay) return replay;
1558
- const from = await this.getRecord({
1559
- record: mutation.relationship.from,
1560
- fields: [],
1561
- });
1562
- const to = await this.getRecord({
1563
- record: mutation.relationship.to,
1564
- fields: [],
1565
- });
1566
- if (!from?.ref.localId || !to?.ref.localId)
1567
- return {
1568
- status: "rejected",
1569
- message: "Native relationship records are unavailable.",
1570
- };
1571
- await assertAccess("crm-record", from.ref.localId, "editor");
1572
- await assertAccess("crm-record", to.ref.localId, "editor");
1573
- const db = getDb();
1574
- const now = new Date().toISOString();
1575
- await db.transaction(async (tx) => {
1576
- const [existing] = await tx
1577
- .select({ id: schema.crmRelationships.id })
1578
- .from(schema.crmRelationships)
1579
- .where(
1580
- and(
1581
- eq(
1582
- schema.crmRelationships.connectionId,
1583
- this.connection.connectionId,
1584
- ),
1585
- eq(schema.crmRelationships.fromRecordId, from.ref.localId!),
1586
- eq(schema.crmRelationships.toRecordId, to.ref.localId!),
1587
- eq(
1588
- schema.crmRelationships.relationshipType,
1589
- mutation.relationship!.relationshipType,
1590
- ),
1591
- accessFilter(
1592
- schema.crmRelationships,
1593
- schema.crmRelationshipShares,
1594
- undefined,
1595
- "editor",
1596
- ),
1597
- ),
1598
- )
1599
- .limit(1);
1600
- if (existing) {
1601
- await assertAccess("crm-relationship", existing.id, "editor");
1602
- await tx
1603
- .update(schema.crmRelationships)
1604
- .set({
1605
- tombstone: mutation.operation === "disassociate",
1606
- label: mutation.relationship!.label ?? null,
1607
- inverseLabel: mutation.relationship!.inverseLabel ?? null,
1608
- sourceField: mutation.relationship!.sourceField ?? null,
1609
- updatedAt: now,
1610
- })
1611
- .where(
1612
- and(
1613
- eq(schema.crmRelationships.id, existing.id),
1614
- accessFilter(
1615
- schema.crmRelationships,
1616
- schema.crmRelationshipShares,
1617
- undefined,
1618
- "editor",
1619
- ),
1620
- ),
1621
- );
1622
- } else if (mutation.operation === "associate") {
1623
- await tx.insert(schema.crmRelationships).values({
1624
- id: crypto.randomUUID(),
1625
- connectionId: this.connection.connectionId,
1626
- fromRecordId: from.ref.localId!,
1627
- toRecordId: to.ref.localId!,
1628
- relationshipType: mutation.relationship!.relationshipType,
1629
- label: mutation.relationship!.label ?? null,
1630
- inverseLabel: mutation.relationship!.inverseLabel ?? null,
1631
- sourceField: mutation.relationship!.sourceField ?? null,
1632
- tombstone: false,
1633
- ownerEmail: this.#nativeConnection.ownerEmail,
1634
- orgId: this.#nativeConnection.orgId,
1635
- visibility: this.#nativeConnection.visibility,
1636
- createdAt: now,
1637
- updatedAt: now,
1638
- });
1639
- }
1640
- await tx.insert(schema.crmMutations).values({
1641
- id: crypto.randomUUID(),
1642
- recordId: from.ref.localId,
1643
- connectionId: this.connection.connectionId,
1644
- operation: mutation.operation,
1645
- initiatedBy: this.#initiatedBy,
1646
- target: "local",
1647
- policyDecision: "execute",
1648
- risk: "routine",
1649
- status: "applied",
1650
- patchJson,
1651
- beforeJson: "{}",
1652
- afterJson: json({ relationship: mutation.operation }),
1653
- idempotencyKey: mutation.idempotencyKey,
1654
- appliedAt: now,
1655
- ownerEmail: this.#nativeConnection.ownerEmail,
1656
- orgId: this.#nativeConnection.orgId,
1657
- visibility: this.#nativeConnection.visibility,
1658
- createdAt: now,
1659
- updatedAt: now,
1660
- });
1661
- });
1662
- return {
1663
- status: "applied",
1664
- record: from,
1665
- remoteRevision: from.remoteRevision,
1666
- };
1667
- }
1668
- }
1669
-
1670
- export async function createNativeCrmAdapter(options: {
1671
- connectionId: string;
1672
- initiatedBy?: "human" | "agent" | "automation";
1673
- accessTier?: "viewer" | "editor";
1674
- }): Promise<NativeCrmAdapter> {
1675
- const accessTier = options.accessTier ?? "editor";
1676
- const connection = await loadNativeConnection(
1677
- options.connectionId,
1678
- accessTier,
1679
- );
1680
- if (accessTier === "editor") {
1681
- await initializeNativeCrmDomain(options.connectionId);
1682
- }
1683
- return new NativeCrmAdapter(connection, options.initiatedBy);
1684
- }
1685
-
1686
- export async function createNativeCrmRecord(input: {
1687
- connectionId: string;
1688
- kind: "account" | "person" | "opportunity";
1689
- displayName: string;
1690
- fields: Record<string, CrmValue>;
1691
- idempotencyKey?: string;
1692
- initiatedBy?: "human" | "agent" | "automation";
1693
- }) {
1694
- const objectType =
1695
- input.kind === "account"
1696
- ? "accounts"
1697
- : input.kind === "person"
1698
- ? "people"
1699
- : "opportunities";
1700
- const adapter = await createNativeCrmAdapter({
1701
- connectionId: input.connectionId,
1702
- initiatedBy: input.initiatedBy,
1703
- });
1704
- return adapter.applyMutation({
1705
- operation: "create",
1706
- record: {
1707
- ...adapter.connection,
1708
- objectType,
1709
- kind: input.kind,
1710
- remoteId: `native-${crypto.randomUUID()}`,
1711
- },
1712
- fields: { ...input.fields, displayName: input.displayName },
1713
- idempotencyKey: input.idempotencyKey ?? crypto.randomUUID(),
1714
- });
1715
- }