@agent-native/core 0.114.16 → 0.115.3

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 (476) hide show
  1. package/agent-native.eject.json +2 -0
  2. package/corpus/README.md +3 -3
  3. package/corpus/core/CHANGELOG.md +41 -0
  4. package/corpus/core/agent-native.eject.json +2 -0
  5. package/corpus/core/docs/content/agent-native-toolkit.mdx +16 -0
  6. package/corpus/core/docs/content/recurring-jobs.mdx +35 -6
  7. package/corpus/core/export-snapshot.json +4 -0
  8. package/corpus/core/package.json +5 -1
  9. package/corpus/core/src/action.ts +22 -1
  10. package/corpus/core/src/agent/production-agent.ts +122 -3
  11. package/corpus/core/src/cli/skills-content/visual-plan-skill.ts +19 -2
  12. package/corpus/core/src/cli/templates-meta.ts +12 -0
  13. package/corpus/core/src/client/AssistantChat.tsx +53 -26
  14. package/corpus/core/src/client/agent-page/AgentJobsTab.tsx +19 -0
  15. package/corpus/core/src/client/agent-page/use-jobs.ts +1 -0
  16. package/corpus/core/src/client/blocks/library/wireframe.tsx +3 -1
  17. package/corpus/core/src/client/chat/message-components.tsx +35 -1
  18. package/corpus/core/src/client/chat/tool-call-display.tsx +38 -3
  19. package/corpus/core/src/connections/catalog.ts +20 -1
  20. package/corpus/core/src/connections/reader.ts +65 -0
  21. package/corpus/core/src/dashboard-storage/index.ts +27 -0
  22. package/corpus/core/src/dashboard-storage/panel-source.ts +168 -0
  23. package/corpus/core/src/dashboard-storage/schema.ts +62 -0
  24. package/corpus/core/src/dashboard-storage/store.ts +333 -0
  25. package/corpus/core/src/deploy/build.ts +95 -16
  26. package/corpus/core/src/eject/provider-api-definitions.ts +1 -0
  27. package/corpus/core/src/eject/workspace-connections.ts +1 -0
  28. package/corpus/core/src/extensions/actions.ts +199 -73
  29. package/corpus/core/src/jobs/actions/list-recurring-jobs.ts +2 -0
  30. package/corpus/core/src/jobs/scheduler.ts +71 -4
  31. package/corpus/core/src/jobs/tools.ts +31 -1
  32. package/corpus/core/src/localization/default-messages.ts +1 -0
  33. package/corpus/core/src/mcp-client/index.ts +6 -0
  34. package/corpus/core/src/provider-api/actions/provider-api.ts +1 -1
  35. package/corpus/core/src/provider-api/index.ts +190 -5
  36. package/corpus/core/src/secrets/register-framework-secrets.ts +8 -0
  37. package/corpus/core/src/server/agent-chat/context-tools.ts +9 -0
  38. package/corpus/core/src/server/agent-chat/framework-prompts.ts +2 -2
  39. package/corpus/core/src/server/agent-chat-plugin.ts +22 -2
  40. package/corpus/core/src/server/core-routes-plugin.ts +1 -0
  41. package/corpus/core/src/server/credential-provider.ts +9 -3
  42. package/corpus/core/src/server/workspace-provider-oauth.ts +181 -4
  43. package/corpus/core/src/styles/agent-native.css +45 -2
  44. package/corpus/core/src/templates/default/.agents/skills/agent-native-docs/SKILL.md +27 -0
  45. package/corpus/core/src/templates/default/.agents/skills/app-branding/SKILL.md +159 -0
  46. package/corpus/core/src/templates/default/.agents/skills/app-permissions/SKILL.md +155 -0
  47. package/corpus/core/src/templates/headless/.agents/skills/agent-native-docs/SKILL.md +27 -0
  48. package/corpus/core/src/templates/workspace-core/.agents/skills/agent-native-docs/SKILL.md +27 -0
  49. package/corpus/core/src/templates/workspace-core/.agents/skills/extensions/SKILL.md +31 -18
  50. package/corpus/core/src/templates/workspace-core/.agents/skills/recurring-jobs/SKILL.md +15 -0
  51. package/corpus/core/src/triggers/actions.ts +11 -2
  52. package/corpus/core/src/triggers/dispatcher.ts +20 -1
  53. package/corpus/core/src/triggers/types.ts +5 -0
  54. package/corpus/core/src/vite/action-types-plugin.ts +9 -1
  55. package/corpus/templates/analytics/.agents/skills/agent-native-docs/SKILL.md +115 -0
  56. package/corpus/templates/analytics/AGENTS.md +11 -11
  57. package/corpus/templates/analytics/_gitignore +39 -0
  58. package/corpus/templates/analytics/actions/query-dashboard-panel.ts +30 -0
  59. package/corpus/templates/analytics/actions/update-dashboard.ts +2 -1
  60. package/corpus/templates/analytics/app/lib/sql-query.ts +11 -25
  61. package/corpus/templates/analytics/changelog/2026-07-21-fixed-extension-updates-failing-before-changes-could-be-appl.md +6 -0
  62. package/corpus/templates/analytics/server/lib/dashboard-panel-query.ts +1 -0
  63. package/corpus/templates/analytics/server/lib/dashboard-panel-source-resolver.ts +51 -0
  64. package/corpus/templates/analytics/server/lib/prometheus.ts +1 -1
  65. package/corpus/templates/analytics/server/plugins/agent-chat.ts +1 -1
  66. package/corpus/templates/assets/.agents/skills/agent-native-docs/SKILL.md +115 -0
  67. package/corpus/templates/brain/.agents/skills/agent-native-docs/SKILL.md +115 -0
  68. package/corpus/templates/calendar/.agents/skills/agent-native-docs/SKILL.md +115 -0
  69. package/corpus/templates/calendar/_gitignore +39 -0
  70. package/corpus/templates/chat/.agents/skills/agent-native-docs/SKILL.md +27 -0
  71. package/corpus/templates/chat/AGENTS.md +1 -1
  72. package/corpus/templates/clips/.agents/skills/agent-native-docs/SKILL.md +115 -0
  73. package/corpus/templates/clips/AGENTS.md +7 -0
  74. package/corpus/templates/clips/actions/prepare-crm-call-evidence.ts +58 -0
  75. package/corpus/templates/clips/app/components/recorder/recorder-engine.ts +88 -11
  76. package/corpus/templates/clips/changelog/2026-07-21-crm-call-evidence-handoff.md +6 -0
  77. package/corpus/templates/clips/changelog/2026-07-21-recording-retries-and-restarts-keep-their-upload-session-so-.md +6 -0
  78. package/corpus/templates/clips/chrome-extension/src/background.ts +20 -5
  79. package/corpus/templates/clips/chrome-extension/src/native-recording-state.ts +19 -0
  80. package/corpus/templates/clips/chrome-extension/src/offscreen.ts +8 -0
  81. package/corpus/templates/clips/desktop/src/overlays/recording-pill.tsx +9 -0
  82. package/corpus/templates/clips/desktop/src-tauri/src/recording_indicator.rs +54 -28
  83. package/corpus/templates/clips/server/plugins/agent-chat.ts +1 -0
  84. package/corpus/templates/content/.agents/skills/agent-native-docs/SKILL.md +115 -0
  85. package/corpus/templates/content/_gitignore +39 -0
  86. package/corpus/templates/content/learnings.defaults.md +5 -0
  87. package/corpus/templates/crm/.agents/skills/agent-native-docs/SKILL.md +115 -0
  88. package/corpus/templates/crm/.agents/skills/agent-native-toolkit/SKILL.md +133 -0
  89. package/corpus/templates/crm/.agents/skills/crm/SKILL.md +128 -0
  90. package/corpus/templates/crm/.agents/skills/customizing-agent-native/SKILL.md +145 -0
  91. package/corpus/templates/crm/.agents/skills/feature-flags/SKILL.md +169 -0
  92. package/corpus/templates/crm/.agents/skills/upgrade-agent-native/SKILL.md +100 -0
  93. package/corpus/templates/crm/.env.example +5 -0
  94. package/corpus/templates/crm/AGENTS.md +120 -0
  95. package/corpus/templates/crm/CHANGELOG.md +1 -0
  96. package/corpus/templates/crm/DEVELOPING.md +66 -0
  97. package/corpus/templates/crm/README.md +106 -0
  98. package/corpus/templates/crm/_gitignore +12 -0
  99. package/corpus/templates/crm/actions/_crm-action-utils.ts +87 -0
  100. package/corpus/templates/crm/actions/_crm-dashboard.ts +69 -0
  101. package/corpus/templates/crm/actions/_crm-signal-utils.ts +79 -0
  102. package/corpus/templates/crm/actions/apply-crm-proposals.ts +203 -0
  103. package/corpus/templates/crm/actions/attach-call-evidence.ts +170 -0
  104. package/corpus/templates/crm/actions/configure-crm-connection.ts +126 -0
  105. package/corpus/templates/crm/actions/configure-native-crm.ts +44 -0
  106. package/corpus/templates/crm/actions/create-crm-record.ts +148 -0
  107. package/corpus/templates/crm/actions/create-crm-signal-tracker.ts +58 -0
  108. package/corpus/templates/crm/actions/delete-staged-dataset.ts +14 -0
  109. package/corpus/templates/crm/actions/get-crm-automation-recipe.ts +36 -0
  110. package/corpus/templates/crm/actions/get-crm-dashboard-panel.ts +33 -0
  111. package/corpus/templates/crm/actions/get-crm-dashboard.ts +14 -0
  112. package/corpus/templates/crm/actions/get-crm-overview.ts +13 -0
  113. package/corpus/templates/crm/actions/get-crm-pipeline-data.ts +61 -0
  114. package/corpus/templates/crm/actions/get-crm-record.ts +75 -0
  115. package/corpus/templates/crm/actions/install-crm-pipeline-dashboard.ts +81 -0
  116. package/corpus/templates/crm/actions/list-crm-dashboard-revisions.ts +15 -0
  117. package/corpus/templates/crm/actions/list-crm-dashboards.ts +15 -0
  118. package/corpus/templates/crm/actions/list-crm-proposals.ts +31 -0
  119. package/corpus/templates/crm/actions/list-crm-records.ts +79 -0
  120. package/corpus/templates/crm/actions/list-crm-saved-views.ts +15 -0
  121. package/corpus/templates/crm/actions/list-crm-signal-hits.ts +43 -0
  122. package/corpus/templates/crm/actions/list-crm-signal-trackers.ts +32 -0
  123. package/corpus/templates/crm/actions/list-crm-tasks.ts +22 -0
  124. package/corpus/templates/crm/actions/list-staged-datasets.ts +14 -0
  125. package/corpus/templates/crm/actions/list-workspace-connections.ts +47 -0
  126. package/corpus/templates/crm/actions/manage-crm-signal-tracker.ts +87 -0
  127. package/corpus/templates/crm/actions/manage-crm-task.ts +132 -0
  128. package/corpus/templates/crm/actions/navigate.ts +63 -0
  129. package/corpus/templates/crm/actions/provider-api-catalog.ts +22 -0
  130. package/corpus/templates/crm/actions/provider-api-docs.ts +54 -0
  131. package/corpus/templates/crm/actions/provider-api-request.ts +92 -0
  132. package/corpus/templates/crm/actions/query-staged-dataset.ts +44 -0
  133. package/corpus/templates/crm/actions/record-crm-call-insight.ts +160 -0
  134. package/corpus/templates/crm/actions/record-crm-smart-signal.ts +150 -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-saved-view-program.ts +103 -0
  138. package/corpus/templates/crm/actions/run-crm-signal-trackers.ts +275 -0
  139. package/corpus/templates/crm/actions/run.ts +11 -0
  140. package/corpus/templates/crm/actions/save-crm-dashboard.ts +51 -0
  141. package/corpus/templates/crm/actions/save-crm-saved-view.ts +119 -0
  142. package/corpus/templates/crm/actions/sync-crm.ts +146 -0
  143. package/corpus/templates/crm/actions/update-crm-record.ts +508 -0
  144. package/corpus/templates/crm/actions/view-screen.ts +218 -0
  145. package/corpus/templates/crm/agent-native.app-skill.json +58 -0
  146. package/corpus/templates/crm/app/components/crm/CreateCrmRecordDialog.tsx +158 -0
  147. package/corpus/templates/crm/app/components/crm/CrmDashboardPanel.tsx +83 -0
  148. package/corpus/templates/crm/app/components/crm/CrmSignalsPanel.tsx +225 -0
  149. package/corpus/templates/crm/app/components/crm/IntelligenceSettings.tsx +413 -0
  150. package/corpus/templates/crm/app/components/crm/RecordActions.tsx +510 -0
  151. package/corpus/templates/crm/app/components/crm/RecordGrid.tsx +134 -0
  152. package/corpus/templates/crm/app/components/crm/RecordWorkspace.tsx +367 -0
  153. package/corpus/templates/crm/app/components/crm/SavedViewDataProgram.tsx +92 -0
  154. package/corpus/templates/crm/app/components/crm/Surface.tsx +96 -0
  155. package/corpus/templates/crm/app/components/crm/WorkOverview.tsx +180 -0
  156. package/corpus/templates/crm/app/components/layout/CrmLayout.tsx +106 -0
  157. package/corpus/templates/crm/app/components/layout/CrmSidebar.tsx +84 -0
  158. package/corpus/templates/crm/app/entry.client.tsx +16 -0
  159. package/corpus/templates/crm/app/entry.server.tsx +10 -0
  160. package/corpus/templates/crm/app/global.css +167 -0
  161. package/corpus/templates/crm/app/hooks/use-navigation-state.ts +60 -0
  162. package/corpus/templates/crm/app/i18n/en-US.ts +89 -0
  163. package/corpus/templates/crm/app/i18n/index.ts +34 -0
  164. package/corpus/templates/crm/app/lib/dashboard.ts +13 -0
  165. package/corpus/templates/crm/app/lib/navigation.ts +54 -0
  166. package/corpus/templates/crm/app/lib/tab-id.ts +1 -0
  167. package/corpus/templates/crm/app/lib/types.ts +172 -0
  168. package/corpus/templates/crm/app/lib/utils.ts +1 -0
  169. package/corpus/templates/crm/app/root.tsx +88 -0
  170. package/corpus/templates/crm/app/routes/_index.tsx +26 -0
  171. package/corpus/templates/crm/app/routes/accounts.tsx +41 -0
  172. package/corpus/templates/crm/app/routes/agent.tsx +5 -0
  173. package/corpus/templates/crm/app/routes/ask.tsx +53 -0
  174. package/corpus/templates/crm/app/routes/dashboard.tsx +118 -0
  175. package/corpus/templates/crm/app/routes/opportunities.tsx +41 -0
  176. package/corpus/templates/crm/app/routes/people.tsx +41 -0
  177. package/corpus/templates/crm/app/routes/proposals.tsx +266 -0
  178. package/corpus/templates/crm/app/routes/records.$recordId.tsx +35 -0
  179. package/corpus/templates/crm/app/routes/settings.tsx +63 -0
  180. package/corpus/templates/crm/app/routes/setup.tsx +308 -0
  181. package/corpus/templates/crm/app/routes/tasks.tsx +252 -0
  182. package/corpus/templates/crm/app/routes/views.tsx +333 -0
  183. package/corpus/templates/crm/app/routes.ts +17 -0
  184. package/corpus/templates/crm/app/vite-env.d.ts +6 -0
  185. package/corpus/templates/crm/changelog/2026-07-21-agent-native-crm.md +6 -0
  186. package/corpus/templates/crm/changelog/2026-07-21-clips-call-evidence-review-recipe.md +6 -0
  187. package/corpus/templates/crm/changelog/2026-07-21-crm-can-now-find-and-review-evidence-grounded-call-signals-w.md +6 -0
  188. package/corpus/templates/crm/changelog/2026-07-21-crm-can-now-manage-keyword-and-smart-call-signal-trackers-fr.md +6 -0
  189. package/corpus/templates/crm/changelog/2026-07-21-delegated-local-crm-automation.md +6 -0
  190. package/corpus/templates/crm/changelog/2026-07-21-native-sql.md +6 -0
  191. package/corpus/templates/crm/changelog/2026-07-21-pipeline-dashboard.md +6 -0
  192. package/corpus/templates/crm/changelog/2026-07-21-pipeline-dashboards-now-install-correctly-from-the-crm-actio.md +6 -0
  193. package/corpus/templates/crm/changelog/2026-07-21-salesforce-connection.md +6 -0
  194. package/corpus/templates/crm/components.json +20 -0
  195. package/corpus/templates/crm/docs/architecture/crm-contract.md +180 -0
  196. package/corpus/templates/crm/learnings.defaults.md +5 -0
  197. package/corpus/templates/crm/package.json +59 -0
  198. package/corpus/templates/crm/public/favicon.svg +1 -0
  199. package/corpus/templates/crm/public/icon-180.svg +1 -0
  200. package/corpus/templates/crm/public/manifest.json +11 -0
  201. package/corpus/templates/crm/react-router.config.ts +7 -0
  202. package/corpus/templates/crm/server/crm/adapter.ts +32 -0
  203. package/corpus/templates/crm/server/crm/crm-field-firewall.ts +64 -0
  204. package/corpus/templates/crm/server/crm/crm-mirror.ts +711 -0
  205. package/corpus/templates/crm/server/crm/hubspot-adapter.ts +1143 -0
  206. package/corpus/templates/crm/server/crm/native-adapter.ts +1715 -0
  207. package/corpus/templates/crm/server/crm/read-through.ts +207 -0
  208. package/corpus/templates/crm/server/crm/salesforce-adapter.ts +1255 -0
  209. package/corpus/templates/crm/server/db/crm-store.ts +1170 -0
  210. package/corpus/templates/crm/server/db/index.ts +169 -0
  211. package/corpus/templates/crm/server/db/schema.ts +441 -0
  212. package/corpus/templates/crm/server/lib/intelligence/default-detectors.ts +65 -0
  213. package/corpus/templates/crm/server/lib/intelligence/evidence.ts +105 -0
  214. package/corpus/templates/crm/server/lib/intelligence/keyword-detector.ts +100 -0
  215. package/corpus/templates/crm/server/lib/intelligence/smart-detector.ts +144 -0
  216. package/corpus/templates/crm/server/lib/intelligence/summary.ts +141 -0
  217. package/corpus/templates/crm/server/lib/provider-api.ts +64 -0
  218. package/corpus/templates/crm/server/middleware/auth.ts +4 -0
  219. package/corpus/templates/crm/server/plugins/agent-chat.ts +68 -0
  220. package/corpus/templates/crm/server/plugins/auth.ts +14 -0
  221. package/corpus/templates/crm/server/plugins/core-routes.ts +17 -0
  222. package/corpus/templates/crm/server/plugins/db.ts +454 -0
  223. package/corpus/templates/crm/server/routes/[...page].get.ts +5 -0
  224. package/corpus/templates/crm/shared/crm-automation-recipes.ts +132 -0
  225. package/corpus/templates/crm/shared/crm-contract.ts +254 -0
  226. package/corpus/templates/crm/shared/crm-sales-config.ts +63 -0
  227. package/corpus/templates/crm/ssr-entry.ts +11 -0
  228. package/corpus/templates/crm/tsconfig.json +24 -0
  229. package/corpus/templates/crm/vite.config.ts +15 -0
  230. package/corpus/templates/crm/vitest.config.ts +5 -0
  231. package/corpus/templates/design/.agents/skills/agent-native-docs/SKILL.md +115 -0
  232. package/corpus/templates/dispatch/.agents/skills/agent-native-docs/SKILL.md +115 -0
  233. package/corpus/templates/forms/.agents/skills/agent-native-docs/SKILL.md +115 -0
  234. package/corpus/templates/forms/_gitignore +39 -0
  235. package/corpus/templates/forms/learnings.defaults.md +5 -0
  236. package/corpus/templates/macros/.agents/skills/agent-native-docs/SKILL.md +115 -0
  237. package/corpus/templates/macros/_gitignore +39 -0
  238. package/corpus/templates/macros/learnings.defaults.md +5 -0
  239. package/corpus/templates/mail/.agents/skills/agent-native-docs/SKILL.md +115 -0
  240. package/corpus/templates/mail/_gitignore +39 -0
  241. package/corpus/templates/plan/.agents/skills/agent-native-docs/SKILL.md +115 -0
  242. package/corpus/templates/plan/.agents/skills/visual-plan/SKILL.md +19 -2
  243. package/corpus/templates/plan/AGENTS.md +10 -0
  244. package/corpus/templates/plan/actions/create-prototype-plan.ts +27 -3
  245. package/corpus/templates/plan/actions/get-plan-blocks.ts +1 -1
  246. package/corpus/templates/plan/actions/update-visual-plan.ts +3 -2
  247. package/corpus/templates/plan/app/components/plan/PlanContentRenderer.tsx +18 -4
  248. package/corpus/templates/plan/app/components/plan/PlanVisualSurface.tsx +10 -3
  249. package/corpus/templates/plan/app/components/plan/wireframe/Wireframe.tsx +3 -1
  250. package/corpus/templates/plan/app/i18n/en-US.ts +6 -0
  251. package/corpus/templates/plan/app/lib/create-plan-routing.ts +39 -0
  252. package/corpus/templates/plan/app/pages/PlansPage.tsx +31 -107
  253. package/corpus/templates/plan/changelog/2026-07-22-high-fidelity-mockup-requests-now-preserve-branded-css-use-d.md +6 -0
  254. package/corpus/templates/plan/shared/plan-content.ts +110 -2
  255. package/corpus/templates/slides/.agents/skills/agent-native-docs/SKILL.md +115 -0
  256. package/corpus/templates/slides/_gitignore +39 -0
  257. package/corpus/templates/tasks/.agents/skills/agent-native-docs/SKILL.md +27 -0
  258. package/corpus/templates/tasks/_gitignore +39 -0
  259. package/corpus/toolkit/CHANGELOG.md +12 -0
  260. package/corpus/toolkit/README.md +30 -0
  261. package/corpus/toolkit/agent-native.eject.json +33 -0
  262. package/corpus/toolkit/package.json +15 -1
  263. package/corpus/toolkit/src/dashboard/DataTable.tsx +273 -0
  264. package/corpus/toolkit/src/dashboard/DateRangePicker.tsx +57 -0
  265. package/corpus/toolkit/src/dashboard/GenericChartPanel.tsx +461 -0
  266. package/corpus/toolkit/src/dashboard/MetricCard.tsx +54 -0
  267. package/corpus/toolkit/src/dashboard/StatsCard.tsx +49 -0
  268. package/corpus/toolkit/src/dashboard/dashboard-layout.ts +329 -0
  269. package/corpus/toolkit/src/dashboard/index.ts +6 -0
  270. package/corpus/toolkit/src/index.ts +1 -0
  271. package/corpus/toolkit/src/ui/calendar.tsx +8 -8
  272. package/corpus/toolkit/src/ui/date-picker.tsx +4 -1
  273. package/dist/action.d.ts +14 -1
  274. package/dist/action.d.ts.map +1 -1
  275. package/dist/action.js.map +1 -1
  276. package/dist/agent/production-agent.d.ts +3 -1
  277. package/dist/agent/production-agent.d.ts.map +1 -1
  278. package/dist/agent/production-agent.js +97 -3
  279. package/dist/agent/production-agent.js.map +1 -1
  280. package/dist/cli/skills-content/visual-plan-skill.d.ts +1 -1
  281. package/dist/cli/skills-content/visual-plan-skill.d.ts.map +1 -1
  282. package/dist/cli/skills-content/visual-plan-skill.js +19 -2
  283. package/dist/cli/skills-content/visual-plan-skill.js.map +1 -1
  284. package/dist/cli/templates-meta.d.ts.map +1 -1
  285. package/dist/cli/templates-meta.js +12 -0
  286. package/dist/cli/templates-meta.js.map +1 -1
  287. package/dist/client/AssistantChat.d.ts +4 -0
  288. package/dist/client/AssistantChat.d.ts.map +1 -1
  289. package/dist/client/AssistantChat.js +30 -17
  290. package/dist/client/AssistantChat.js.map +1 -1
  291. package/dist/client/agent-page/AgentJobsTab.d.ts.map +1 -1
  292. package/dist/client/agent-page/AgentJobsTab.js +3 -1
  293. package/dist/client/agent-page/AgentJobsTab.js.map +1 -1
  294. package/dist/client/agent-page/use-jobs.d.ts +1 -0
  295. package/dist/client/agent-page/use-jobs.d.ts.map +1 -1
  296. package/dist/client/agent-page/use-jobs.js.map +1 -1
  297. package/dist/client/blocks/library/wireframe.d.ts.map +1 -1
  298. package/dist/client/blocks/library/wireframe.js +3 -2
  299. package/dist/client/blocks/library/wireframe.js.map +1 -1
  300. package/dist/client/chat/message-components.d.ts +5 -0
  301. package/dist/client/chat/message-components.d.ts.map +1 -1
  302. package/dist/client/chat/message-components.js +17 -1
  303. package/dist/client/chat/message-components.js.map +1 -1
  304. package/dist/client/chat/tool-call-display.d.ts +3 -1
  305. package/dist/client/chat/tool-call-display.d.ts.map +1 -1
  306. package/dist/client/chat/tool-call-display.js +17 -7
  307. package/dist/client/chat/tool-call-display.js.map +1 -1
  308. package/dist/collab/routes.d.ts +1 -1
  309. package/dist/collab/struct-routes.d.ts +1 -1
  310. package/dist/connections/catalog.d.ts +17 -3
  311. package/dist/connections/catalog.d.ts.map +1 -1
  312. package/dist/connections/catalog.js +16 -1
  313. package/dist/connections/catalog.js.map +1 -1
  314. package/dist/connections/reader.d.ts +18 -0
  315. package/dist/connections/reader.d.ts.map +1 -1
  316. package/dist/connections/reader.js +41 -0
  317. package/dist/connections/reader.js.map +1 -1
  318. package/dist/dashboard-storage/index.d.ts +4 -0
  319. package/dist/dashboard-storage/index.d.ts.map +1 -0
  320. package/dist/dashboard-storage/index.js +4 -0
  321. package/dist/dashboard-storage/index.js.map +1 -0
  322. package/dist/dashboard-storage/panel-source.d.ts +68 -0
  323. package/dist/dashboard-storage/panel-source.d.ts.map +1 -0
  324. package/dist/dashboard-storage/panel-source.js +76 -0
  325. package/dist/dashboard-storage/panel-source.js.map +1 -0
  326. package/dist/dashboard-storage/schema.d.ts +562 -0
  327. package/dist/dashboard-storage/schema.d.ts.map +1 -0
  328. package/dist/dashboard-storage/schema.js +32 -0
  329. package/dist/dashboard-storage/schema.js.map +1 -0
  330. package/dist/dashboard-storage/store.d.ts +62 -0
  331. package/dist/dashboard-storage/store.d.ts.map +1 -0
  332. package/dist/dashboard-storage/store.js +227 -0
  333. package/dist/dashboard-storage/store.js.map +1 -0
  334. package/dist/deploy/build.d.ts +7 -0
  335. package/dist/deploy/build.d.ts.map +1 -1
  336. package/dist/deploy/build.js +78 -14
  337. package/dist/deploy/build.js.map +1 -1
  338. package/dist/eject/provider-api-definitions.d.ts +2 -1
  339. package/dist/eject/provider-api-definitions.d.ts.map +1 -1
  340. package/dist/eject/provider-api-definitions.js +1 -0
  341. package/dist/eject/provider-api-definitions.js.map +1 -1
  342. package/dist/eject/workspace-connections.d.ts.map +1 -1
  343. package/dist/eject/workspace-connections.js +1 -0
  344. package/dist/eject/workspace-connections.js.map +1 -1
  345. package/dist/extensions/actions.d.ts.map +1 -1
  346. package/dist/extensions/actions.js +165 -67
  347. package/dist/extensions/actions.js.map +1 -1
  348. package/dist/file-upload/actions/upload-image.d.ts +1 -1
  349. package/dist/jobs/actions/list-recurring-jobs.d.ts +1 -0
  350. package/dist/jobs/actions/list-recurring-jobs.d.ts.map +1 -1
  351. package/dist/jobs/actions/list-recurring-jobs.js +1 -0
  352. package/dist/jobs/actions/list-recurring-jobs.js.map +1 -1
  353. package/dist/jobs/scheduler.d.ts +17 -2
  354. package/dist/jobs/scheduler.d.ts.map +1 -1
  355. package/dist/jobs/scheduler.js +51 -2
  356. package/dist/jobs/scheduler.js.map +1 -1
  357. package/dist/jobs/tools.d.ts.map +1 -1
  358. package/dist/jobs/tools.js +32 -2
  359. package/dist/jobs/tools.js.map +1 -1
  360. package/dist/localization/default-messages.d.ts +1 -0
  361. package/dist/localization/default-messages.d.ts.map +1 -1
  362. package/dist/localization/default-messages.js +1 -0
  363. package/dist/localization/default-messages.js.map +1 -1
  364. package/dist/mcp-client/index.d.ts +2 -0
  365. package/dist/mcp-client/index.d.ts.map +1 -1
  366. package/dist/mcp-client/index.js +5 -0
  367. package/dist/mcp-client/index.js.map +1 -1
  368. package/dist/notifications/routes.d.ts +3 -3
  369. package/dist/progress/routes.d.ts +1 -1
  370. package/dist/provider-api/actions/custom-provider-registration.d.ts +8 -8
  371. package/dist/provider-api/actions/provider-api.d.ts +8 -6
  372. package/dist/provider-api/actions/provider-api.d.ts.map +1 -1
  373. package/dist/provider-api/actions/provider-api.js +1 -1
  374. package/dist/provider-api/actions/provider-api.js.map +1 -1
  375. package/dist/provider-api/index.d.ts +11 -7
  376. package/dist/provider-api/index.d.ts.map +1 -1
  377. package/dist/provider-api/index.js +143 -5
  378. package/dist/provider-api/index.js.map +1 -1
  379. package/dist/resources/handlers.d.ts +1 -1
  380. package/dist/secrets/register-framework-secrets.d.ts.map +1 -1
  381. package/dist/secrets/register-framework-secrets.js +7 -0
  382. package/dist/secrets/register-framework-secrets.js.map +1 -1
  383. package/dist/server/agent-chat/context-tools.d.ts.map +1 -1
  384. package/dist/server/agent-chat/context-tools.js +9 -0
  385. package/dist/server/agent-chat/context-tools.js.map +1 -1
  386. package/dist/server/agent-chat/framework-prompts.js +2 -2
  387. package/dist/server/agent-chat/framework-prompts.js.map +1 -1
  388. package/dist/server/agent-chat-plugin.d.ts.map +1 -1
  389. package/dist/server/agent-chat-plugin.js +18 -2
  390. package/dist/server/agent-chat-plugin.js.map +1 -1
  391. package/dist/server/core-routes-plugin.d.ts.map +1 -1
  392. package/dist/server/core-routes-plugin.js +1 -0
  393. package/dist/server/core-routes-plugin.js.map +1 -1
  394. package/dist/server/credential-provider.d.ts +4 -3
  395. package/dist/server/credential-provider.d.ts.map +1 -1
  396. package/dist/server/credential-provider.js +9 -3
  397. package/dist/server/credential-provider.js.map +1 -1
  398. package/dist/server/workspace-provider-oauth.d.ts +6 -1
  399. package/dist/server/workspace-provider-oauth.d.ts.map +1 -1
  400. package/dist/server/workspace-provider-oauth.js +141 -4
  401. package/dist/server/workspace-provider-oauth.js.map +1 -1
  402. package/dist/styles/agent-native.css +45 -2
  403. package/dist/templates/chat/.agents/skills/agent-native-docs/SKILL.md +27 -0
  404. package/dist/templates/chat/AGENTS.md +1 -1
  405. package/dist/templates/default/.agents/skills/agent-native-docs/SKILL.md +27 -0
  406. package/dist/templates/default/.agents/skills/app-branding/SKILL.md +159 -0
  407. package/dist/templates/default/.agents/skills/app-permissions/SKILL.md +155 -0
  408. package/dist/templates/headless/.agents/skills/agent-native-docs/SKILL.md +27 -0
  409. package/dist/templates/workspace-core/.agents/skills/agent-native-docs/SKILL.md +27 -0
  410. package/dist/templates/workspace-core/.agents/skills/extensions/SKILL.md +31 -18
  411. package/dist/templates/workspace-core/.agents/skills/recurring-jobs/SKILL.md +15 -0
  412. package/dist/triggers/actions.d.ts.map +1 -1
  413. package/dist/triggers/actions.js +10 -2
  414. package/dist/triggers/actions.js.map +1 -1
  415. package/dist/triggers/dispatcher.d.ts.map +1 -1
  416. package/dist/triggers/dispatcher.js +15 -0
  417. package/dist/triggers/dispatcher.js.map +1 -1
  418. package/dist/triggers/types.d.ts +5 -0
  419. package/dist/triggers/types.d.ts.map +1 -1
  420. package/dist/triggers/types.js.map +1 -1
  421. package/dist/vite/action-types-plugin.d.ts.map +1 -1
  422. package/dist/vite/action-types-plugin.js +4 -1
  423. package/dist/vite/action-types-plugin.js.map +1 -1
  424. package/docs/content/agent-native-toolkit.mdx +16 -0
  425. package/docs/content/recurring-jobs.mdx +35 -6
  426. package/package.json +7 -3
  427. package/src/action.ts +22 -1
  428. package/src/agent/production-agent.ts +122 -3
  429. package/src/cli/skills-content/visual-plan-skill.ts +19 -2
  430. package/src/cli/templates-meta.ts +12 -0
  431. package/src/client/AssistantChat.tsx +53 -26
  432. package/src/client/agent-page/AgentJobsTab.tsx +19 -0
  433. package/src/client/agent-page/use-jobs.ts +1 -0
  434. package/src/client/blocks/library/wireframe.tsx +3 -1
  435. package/src/client/chat/message-components.tsx +35 -1
  436. package/src/client/chat/tool-call-display.tsx +38 -3
  437. package/src/connections/catalog.ts +20 -1
  438. package/src/connections/reader.ts +65 -0
  439. package/src/dashboard-storage/index.ts +27 -0
  440. package/src/dashboard-storage/panel-source.ts +168 -0
  441. package/src/dashboard-storage/schema.ts +62 -0
  442. package/src/dashboard-storage/store.ts +333 -0
  443. package/src/deploy/build.ts +95 -16
  444. package/src/eject/provider-api-definitions.ts +1 -0
  445. package/src/eject/workspace-connections.ts +1 -0
  446. package/src/extensions/actions.ts +199 -73
  447. package/src/jobs/actions/list-recurring-jobs.ts +2 -0
  448. package/src/jobs/scheduler.ts +71 -4
  449. package/src/jobs/tools.ts +31 -1
  450. package/src/localization/default-messages.ts +1 -0
  451. package/src/mcp-client/index.ts +6 -0
  452. package/src/provider-api/actions/provider-api.ts +1 -1
  453. package/src/provider-api/index.ts +190 -5
  454. package/src/secrets/register-framework-secrets.ts +8 -0
  455. package/src/server/agent-chat/context-tools.ts +9 -0
  456. package/src/server/agent-chat/framework-prompts.ts +2 -2
  457. package/src/server/agent-chat-plugin.ts +22 -2
  458. package/src/server/core-routes-plugin.ts +1 -0
  459. package/src/server/credential-provider.ts +9 -3
  460. package/src/server/workspace-provider-oauth.ts +181 -4
  461. package/src/styles/agent-native.css +45 -2
  462. package/src/templates/chat/.agents/skills/agent-native-docs/SKILL.md +27 -0
  463. package/src/templates/chat/AGENTS.md +1 -1
  464. package/src/templates/default/.agents/skills/agent-native-docs/SKILL.md +27 -0
  465. package/src/templates/default/.agents/skills/app-branding/SKILL.md +159 -0
  466. package/src/templates/default/.agents/skills/app-permissions/SKILL.md +155 -0
  467. package/src/templates/headless/.agents/skills/agent-native-docs/SKILL.md +27 -0
  468. package/src/templates/workspace-core/.agents/skills/agent-native-docs/SKILL.md +27 -0
  469. package/src/templates/workspace-core/.agents/skills/extensions/SKILL.md +31 -18
  470. package/src/templates/workspace-core/.agents/skills/recurring-jobs/SKILL.md +15 -0
  471. package/src/triggers/actions.ts +11 -2
  472. package/src/triggers/dispatcher.ts +20 -1
  473. package/src/triggers/types.ts +5 -0
  474. package/src/vite/action-types-plugin.ts +9 -1
  475. package/corpus/templates/analytics/server/handlers/sql-query.ts +0 -36
  476. package/corpus/templates/analytics/server/routes/api/sql-query.post.ts +0 -1
@@ -0,0 +1,1715 @@
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
+ }