@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,1146 +0,0 @@
1
- import {
2
- resolveWorkspaceConnectionCredentialForApp,
3
- resolveWorkspaceConnectionForApp,
4
- type ResolvedWorkspaceConnectionForApp,
5
- type WorkspaceConnectionForApp,
6
- } from "@agent-native/core/workspace-connections";
7
-
8
- import type {
9
- CrmAccessScope,
10
- CrmAdapter,
11
- CrmAdapterCapabilities,
12
- CrmConnectionRef,
13
- CrmFieldDefinition,
14
- CrmObjectDefinition,
15
- CrmObjectKind,
16
- CrmObjectRef,
17
- CrmProvenance,
18
- CrmRecord,
19
- CrmRecordRef,
20
- CrmRelationship,
21
- CrmSyncPage,
22
- CrmValue,
23
- } from "../../shared/crm-contract.js";
24
-
25
- const HUBSPOT_API_BASE = "https://api.hubapi.com";
26
- const CRM_APP_ID = "crm";
27
- const HUBSPOT_CREDENTIAL_KEYS = [
28
- "HUBSPOT_ACCESS_TOKEN",
29
- "HUBSPOT_PRIVATE_APP_TOKEN",
30
- ] as const;
31
- const CORE_OBJECT_TYPES = ["companies", "contacts", "deals"] as const;
32
- const MAX_PAGE_SIZE = 100;
33
- const MAX_RETRY_ATTEMPTS = 3;
34
- const MAX_RETRY_DELAY_MS = 1_500;
35
- const MAX_SINGLE_RETRY_DELAY_MS = 1_000;
36
- const REQUEST_TIMEOUT_MS = 10_000;
37
-
38
- type HubSpotObjectType = (typeof CORE_OBJECT_TYPES)[number] | string;
39
-
40
- export interface HubSpotTransportRequest {
41
- path: string;
42
- method?: "GET" | "POST" | "PATCH" | "PUT" | "DELETE";
43
- body?: unknown;
44
- headers?: Record<string, string>;
45
- retrySafe?: boolean;
46
- }
47
-
48
- export interface HubSpotTransportResponse {
49
- status: number;
50
- body?: unknown;
51
- headers?: Record<string, string | undefined>;
52
- }
53
-
54
- export interface HubSpotTransport {
55
- request(input: HubSpotTransportRequest): Promise<HubSpotTransportResponse>;
56
- }
57
-
58
- export interface CreateHubSpotCrmAdapterOptions {
59
- connectionId?: string;
60
- userEmail?: string;
61
- orgId?: string | null;
62
- transport?: HubSpotTransport;
63
- }
64
-
65
- export interface HubSpotCrmAdapterOptions {
66
- connection: WorkspaceConnectionForApp;
67
- transport: HubSpotTransport;
68
- accessScope?: CrmAccessScope;
69
- }
70
-
71
- type HubSpotProperty = {
72
- name?: string;
73
- label?: string;
74
- type?: string;
75
- fieldType?: string;
76
- hidden?: boolean;
77
- readOnlyValue?: boolean;
78
- calculated?: boolean;
79
- required?: boolean;
80
- options?: Array<{ value?: string; label?: string; hidden?: boolean }>;
81
- referencedObjectType?: string;
82
- sensitiveDataCategory?: string;
83
- };
84
-
85
- type HubSpotSchema = {
86
- objectTypeId?: string | number;
87
- name?: string;
88
- labels?: { singular?: string; plural?: string };
89
- metaType?: string;
90
- properties?: HubSpotProperty[];
91
- };
92
-
93
- type HubSpotRecord = {
94
- id?: string | number;
95
- properties?: Record<string, unknown>;
96
- createdAt?: string;
97
- updatedAt?: string;
98
- archived?: boolean;
99
- };
100
-
101
- type HubSpotListResponse = {
102
- results?: HubSpotRecord[];
103
- paging?: { next?: { after?: string } };
104
- };
105
-
106
- type HubSpotAssociation = {
107
- toObjectId?: string | number;
108
- id?: string | number;
109
- associationTypes?: Array<{
110
- associationCategory?: string;
111
- associationTypeId?: string | number;
112
- label?: string | null;
113
- }>;
114
- };
115
-
116
- type HubSpotAssociationResponse = {
117
- results?: HubSpotAssociation[];
118
- paging?: { next?: { after?: string } };
119
- };
120
-
121
- function nonEmpty(value: string | undefined | null): string | undefined {
122
- const trimmed = value?.trim();
123
- return trimmed || undefined;
124
- }
125
-
126
- function uniqueStrings(values: readonly string[]): string[] {
127
- return Array.from(
128
- new Set(values.map((value) => value.trim()).filter(Boolean)),
129
- );
130
- }
131
-
132
- function objectKind(objectType: string): CrmObjectKind {
133
- switch (objectType.toLowerCase()) {
134
- case "companies":
135
- return "account";
136
- case "contacts":
137
- return "person";
138
- case "deals":
139
- return "opportunity";
140
- case "tasks":
141
- return "task";
142
- case "calls":
143
- case "emails":
144
- case "meetings":
145
- case "notes":
146
- return "activity";
147
- default:
148
- return "custom";
149
- }
150
- }
151
-
152
- function objectDisplayProperty(objectType: string): string[] {
153
- switch (objectType.toLowerCase()) {
154
- case "companies":
155
- return ["name", "domain"];
156
- case "contacts":
157
- return ["firstname", "lastname", "email"];
158
- case "deals":
159
- return ["dealname"];
160
- case "tickets":
161
- return ["subject"];
162
- default:
163
- return ["name", "hs_object_id"];
164
- }
165
- }
166
-
167
- function propertyValueType(
168
- property: HubSpotProperty,
169
- ): CrmFieldDefinition["valueType"] {
170
- const type = property.type?.toLowerCase();
171
- const fieldType = property.fieldType?.toLowerCase();
172
- if (fieldType === "checkbox") return "multi-enum";
173
- if (type === "enumeration") return "enum";
174
- if (type === "bool" || type === "boolean") return "boolean";
175
- if (type === "number") return "number";
176
- if (type === "date") return "date";
177
- if (type === "datetime") return "datetime";
178
- if (type === "json") return "json";
179
- if (fieldType === "currency" || property.name === "amount") return "currency";
180
- if (fieldType === "percentage") return "percent";
181
- if (type === "object_coordinates" || type === "reference") return "reference";
182
- return "string";
183
- }
184
-
185
- function propertyIsSensitive(property: HubSpotProperty): boolean {
186
- const category = property.sensitiveDataCategory?.trim().toLowerCase();
187
- return Boolean(
188
- category && category !== "none" && category !== "not_sensitive",
189
- );
190
- }
191
-
192
- function scalarValue(value: unknown): CrmValue | undefined {
193
- if (
194
- value === null ||
195
- typeof value === "string" ||
196
- typeof value === "number" ||
197
- typeof value === "boolean"
198
- ) {
199
- return value;
200
- }
201
- if (Array.isArray(value)) {
202
- const values = value.filter(
203
- (item): item is string | number | boolean | null =>
204
- item === null ||
205
- typeof item === "string" ||
206
- typeof item === "number" ||
207
- typeof item === "boolean",
208
- );
209
- return values.length === value.length ? values : undefined;
210
- }
211
- return undefined;
212
- }
213
-
214
- function toQuery(params: Record<string, string | undefined>): string {
215
- const query = new URLSearchParams();
216
- for (const [key, value] of Object.entries(params)) {
217
- if (value) query.set(key, value);
218
- }
219
- const value = query.toString();
220
- return value ? `?${value}` : "";
221
- }
222
-
223
- function encodeCursorState(value: { index: number; after?: string }): string {
224
- const bytes = new TextEncoder().encode(JSON.stringify(value));
225
- let binary = "";
226
- for (const byte of bytes) binary += String.fromCharCode(byte);
227
- return btoa(binary)
228
- .replace(/\+/g, "-")
229
- .replace(/\//g, "_")
230
- .replace(/=+$/, "");
231
- }
232
-
233
- function decodedCursor(cursor: string | undefined): {
234
- index: number;
235
- after?: string;
236
- } {
237
- if (!cursor) return { index: 0 };
238
- try {
239
- const padded = cursor
240
- .replace(/-/g, "+")
241
- .replace(/_/g, "/")
242
- .padEnd(Math.ceil(cursor.length / 4) * 4, "=");
243
- const binary = atob(padded);
244
- const decoded = JSON.parse(
245
- new TextDecoder().decode(
246
- Uint8Array.from(binary, (character) => character.charCodeAt(0)),
247
- ),
248
- ) as { index?: unknown; after?: unknown };
249
- const index = typeof decoded.index === "number" ? decoded.index : 0;
250
- return {
251
- index: Number.isInteger(index) && index >= 0 ? index : 0,
252
- ...(typeof decoded.after === "string" && decoded.after
253
- ? { after: decoded.after }
254
- : {}),
255
- };
256
- } catch {
257
- // HubSpot's `after` values are already opaque; accept them for one-object callers.
258
- return { index: 0, after: cursor };
259
- }
260
- }
261
-
262
- function encodedCursor(index: number, after?: string): string | undefined {
263
- if (!after) return undefined;
264
- return encodeCursorState({ index, after });
265
- }
266
-
267
- function scopeAllows(
268
- connection: WorkspaceConnectionForApp,
269
- objectType: string,
270
- operation: "read" | "write",
271
- ): boolean {
272
- const scopes = [
273
- ...connection.scopes,
274
- ...(connection.explicitGrant?.scopes ?? []),
275
- ].map((scope) => scope.toLowerCase());
276
- if (scopes.length === 0) return operation === "read";
277
- const singular = objectType.toLowerCase().replace(/s$/, "");
278
- return scopes.some(
279
- (scope) =>
280
- scope === "all" ||
281
- scope === "crm" ||
282
- scope === `crm.${operation}` ||
283
- scope === `crm.objects.${operation}` ||
284
- scope === `crm.objects.${objectType.toLowerCase()}.${operation}` ||
285
- scope === `crm.objects.${singular}.${operation}` ||
286
- (operation === "write" && scope.endsWith(".write")),
287
- );
288
- }
289
-
290
- function accessScopeFor(
291
- connection: WorkspaceConnectionForApp,
292
- objectType: string,
293
- ): CrmAccessScope {
294
- const grantId = connection.appAccess.grantId ?? undefined;
295
- const mode = [
296
- ...connection.credentialRefs,
297
- ...(connection.explicitGrant?.credentialRefs ?? []),
298
- ].some((ref) => ref.scope === "user")
299
- ? "user"
300
- : "service-account";
301
- const readable = scopeAllows(connection, objectType, "read");
302
- const writable = scopeAllows(connection, objectType, "write");
303
- return {
304
- key: `${connection.id}:${grantId ?? connection.appAccess.mode}`,
305
- actorId: connection.ownerEmail,
306
- grantId,
307
- mode,
308
- objectReadable: readable,
309
- objectCreateable: writable,
310
- objectUpdateable: writable,
311
- objectDeleteable: false,
312
- recordVisibility: mode === "user" ? "actor" : "unknown",
313
- };
314
- }
315
-
316
- function definitionFrom(
317
- connection: WorkspaceConnectionForApp,
318
- objectType: string,
319
- schema: HubSpotSchema | undefined,
320
- properties: HubSpotProperty[],
321
- ): CrmObjectDefinition {
322
- const scope = accessScopeFor(connection, objectType);
323
- const custom =
324
- !CORE_OBJECT_TYPES.includes(
325
- objectType as (typeof CORE_OBJECT_TYPES)[number],
326
- ) && schema?.metaType !== "HUBSPOT";
327
- return {
328
- connectionId: connection.id,
329
- provider: "hubspot",
330
- ...(connection.accountId ? { accountId: connection.accountId } : {}),
331
- actorId: connection.ownerEmail,
332
- objectType,
333
- kind: objectKind(objectType),
334
- label: schema?.labels?.singular || objectType,
335
- pluralLabel: schema?.labels?.plural || objectType,
336
- custom,
337
- queryable: scope.objectReadable,
338
- searchable: scope.objectReadable,
339
- createable: scope.objectCreateable,
340
- updateable: scope.objectUpdateable,
341
- deleteable: false,
342
- fields: properties
343
- .map((property): CrmFieldDefinition | null => {
344
- const name = nonEmpty(property.name);
345
- if (!name) return null;
346
- const sensitive = propertyIsSensitive(property);
347
- return {
348
- name,
349
- label: property.label || name,
350
- valueType: propertyValueType(property),
351
- storagePolicy: sensitive ? "redacted" : "remote-only",
352
- sensitive,
353
- readable: !property.hidden && scope.objectReadable,
354
- createable:
355
- !sensitive &&
356
- !property.readOnlyValue &&
357
- !property.calculated &&
358
- scope.objectCreateable,
359
- updateable:
360
- !sensitive &&
361
- !property.readOnlyValue &&
362
- !property.calculated &&
363
- scope.objectUpdateable,
364
- required: property.required === true,
365
- ...(property.options
366
- ? {
367
- options: property.options
368
- .filter((option) => nonEmpty(option.value))
369
- .map((option) => ({
370
- value: option.value!.trim(),
371
- label: option.label || option.value!.trim(),
372
- ...(option.hidden ? { active: false } : {}),
373
- })),
374
- }
375
- : {}),
376
- ...(property.referencedObjectType
377
- ? { referencedObjectType: property.referencedObjectType }
378
- : {}),
379
- };
380
- })
381
- .filter((field): field is CrmFieldDefinition => field !== null),
382
- };
383
- }
384
-
385
- function recordRef(
386
- connection: WorkspaceConnectionForApp,
387
- objectType: string,
388
- remoteId: string,
389
- ): CrmRecordRef {
390
- return {
391
- connectionId: connection.id,
392
- provider: "hubspot",
393
- ...(connection.accountId ? { accountId: connection.accountId } : {}),
394
- actorId: connection.ownerEmail,
395
- objectType,
396
- kind: objectKind(objectType),
397
- remoteId,
398
- };
399
- }
400
-
401
- function provenance(
402
- connection: WorkspaceConnectionForApp,
403
- objectType: string,
404
- remoteId: string,
405
- remoteRevision: string | undefined,
406
- fieldNames: string[],
407
- ): CrmProvenance[] {
408
- const observedAt = new Date().toISOString();
409
- return fieldNames.map((fieldName) => ({
410
- provider: "hubspot",
411
- connectionId: connection.id,
412
- objectType,
413
- remoteId,
414
- fieldName,
415
- ...(remoteRevision ? { remoteRevision } : {}),
416
- observedAt,
417
- }));
418
- }
419
-
420
- function projectRecord(
421
- connection: WorkspaceConnectionForApp,
422
- objectType: string,
423
- source: HubSpotRecord,
424
- fieldAllowList: string[],
425
- ): CrmRecord | null {
426
- const remoteId = source.id == null ? undefined : String(source.id);
427
- if (!remoteId) return null;
428
- const allowed = uniqueStrings(fieldAllowList);
429
- const fields: Record<string, CrmValue> = {};
430
- for (const fieldName of allowed) {
431
- const value = scalarValue(source.properties?.[fieldName]);
432
- if (value !== undefined) fields[fieldName] = value;
433
- }
434
- const displayValues = objectDisplayProperty(objectType)
435
- .map((fieldName) => fields[fieldName])
436
- .filter((value): value is CrmValue => value !== undefined)
437
- .filter(
438
- (value): value is string | number | boolean =>
439
- typeof value === "string" ||
440
- typeof value === "number" ||
441
- typeof value === "boolean",
442
- );
443
- const displayName =
444
- displayValues.map(String).filter(Boolean).join(" ") || remoteId;
445
- const remoteRevision = source.updatedAt;
446
- return {
447
- ref: recordRef(connection, objectType, remoteId),
448
- displayName,
449
- fields,
450
- ...(remoteRevision ? { remoteRevision } : {}),
451
- ...(source.updatedAt ? { remoteUpdatedAt: source.updatedAt } : {}),
452
- deleted: source.archived === true,
453
- accessScope: accessScopeFor(connection, objectType),
454
- provenance: provenance(
455
- connection,
456
- objectType,
457
- remoteId,
458
- remoteRevision,
459
- Object.keys(fields),
460
- ),
461
- };
462
- }
463
-
464
- function isNotFound(error: unknown): boolean {
465
- return error instanceof HubSpotApiError && error.status === 404;
466
- }
467
-
468
- class HubSpotApiError extends Error {
469
- constructor(
470
- readonly status: number,
471
- message: string,
472
- ) {
473
- super(message);
474
- }
475
- }
476
-
477
- function hubSpotErrorMessage(status: number): string {
478
- if (status === 401 || status === 403)
479
- return "HubSpot authentication or authorization failed.";
480
- if (status === 404) return "HubSpot resource was not found.";
481
- if (status === 429) return "HubSpot rate limit exceeded.";
482
- if (status >= 500) return "HubSpot service is temporarily unavailable.";
483
- return "HubSpot request failed.";
484
- }
485
-
486
- function headerValue(
487
- headers: Record<string, string | undefined> | undefined,
488
- name: string,
489
- ): string | undefined {
490
- if (!headers) return undefined;
491
- const expected = name.toLowerCase();
492
- return Object.entries(headers).find(
493
- ([key]) => key.toLowerCase() === expected,
494
- )?.[1];
495
- }
496
-
497
- export function hubSpotRetryDelayMs(
498
- response: HubSpotTransportResponse,
499
- attempt: number,
500
- elapsedMs: number,
501
- now = Date.now(),
502
- ): number {
503
- const retryAfter = headerValue(response.headers, "retry-after")?.trim();
504
- const numericRetryAfter = retryAfter ? Number(retryAfter) : Number.NaN;
505
- const requestedDelay = Number.isFinite(numericRetryAfter)
506
- ? Math.max(0, numericRetryAfter * 1_000)
507
- : retryAfter
508
- ? Math.max(0, Date.parse(retryAfter) - now)
509
- : 100 * 2 ** attempt;
510
- return Math.max(
511
- 0,
512
- Math.min(
513
- requestedDelay,
514
- MAX_SINGLE_RETRY_DELAY_MS,
515
- MAX_RETRY_DELAY_MS - elapsedMs,
516
- ),
517
- );
518
- }
519
-
520
- function canRetryHubSpotResponse(status: number): boolean {
521
- return status === 429 || status >= 500;
522
- }
523
-
524
- function assertOwnedRecord(
525
- connection: WorkspaceConnectionForApp,
526
- record: CrmRecordRef,
527
- ): void {
528
- if (record.provider !== "hubspot" || record.connectionId !== connection.id) {
529
- throw new Error(
530
- "HubSpot record does not belong to this workspace connection.",
531
- );
532
- }
533
- }
534
-
535
- class FetchHubSpotTransport implements HubSpotTransport {
536
- constructor(private readonly token: string) {}
537
-
538
- async request(
539
- input: HubSpotTransportRequest,
540
- ): Promise<HubSpotTransportResponse> {
541
- const response = await fetch(`${HUBSPOT_API_BASE}${input.path}`, {
542
- method: input.method ?? "GET",
543
- signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
544
- headers: {
545
- Authorization: `Bearer ${this.token}`,
546
- ...(input.body === undefined
547
- ? {}
548
- : { "Content-Type": "application/json" }),
549
- ...input.headers,
550
- },
551
- ...(input.body === undefined ? {} : { body: JSON.stringify(input.body) }),
552
- });
553
- const text = await response.text();
554
- let body: unknown;
555
- if (text) {
556
- try {
557
- body = JSON.parse(text);
558
- } catch {
559
- body = text;
560
- }
561
- }
562
- return {
563
- status: response.status,
564
- body,
565
- headers: {
566
- "retry-after": response.headers.get("retry-after") ?? undefined,
567
- },
568
- };
569
- }
570
- }
571
-
572
- export class HubSpotCrmAdapter implements CrmAdapter {
573
- readonly connection: CrmConnectionRef;
574
- readonly capabilities: CrmAdapterCapabilities = {
575
- schemaDiscovery: true,
576
- customObjects: true,
577
- search: true,
578
- incrementalSync: true,
579
- deletedRecordSync: true,
580
- conditionalMutations: false,
581
- labeledRelationships: true,
582
- perFieldPermissions: false,
583
- perRecordPermissions: false,
584
- };
585
-
586
- private readonly workspaceConnection: WorkspaceConnectionForApp;
587
- private readonly transport: HubSpotTransport;
588
-
589
- constructor(options: HubSpotCrmAdapterOptions) {
590
- this.workspaceConnection = options.connection;
591
- this.transport = options.transport;
592
- this.connection = {
593
- connectionId: options.connection.id,
594
- provider: "hubspot",
595
- ...(options.connection.accountId
596
- ? { accountId: options.connection.accountId }
597
- : {}),
598
- actorId: options.connection.ownerEmail,
599
- };
600
- }
601
-
602
- private async request<T>(input: HubSpotTransportRequest): Promise<T> {
603
- const readRetrySafe =
604
- input.retrySafe === true ||
605
- input.method === undefined ||
606
- input.method === "GET";
607
- let elapsedDelayMs = 0;
608
- for (let attempt = 0; attempt < MAX_RETRY_ATTEMPTS; attempt++) {
609
- const response = await this.transport.request(input);
610
- if (response.status >= 200 && response.status < 300) {
611
- return (response.body ?? {}) as T;
612
- }
613
- const mayRetry =
614
- attempt + 1 < MAX_RETRY_ATTEMPTS &&
615
- readRetrySafe &&
616
- canRetryHubSpotResponse(response.status) &&
617
- elapsedDelayMs < MAX_RETRY_DELAY_MS;
618
- if (!mayRetry) {
619
- throw new HubSpotApiError(
620
- response.status,
621
- `HubSpot API error ${response.status}: ${hubSpotErrorMessage(response.status)}`,
622
- );
623
- }
624
- const delay = hubSpotRetryDelayMs(response, attempt, elapsedDelayMs);
625
- if (delay <= 0) {
626
- continue;
627
- }
628
- await new Promise<void>((resolve) => setTimeout(resolve, delay));
629
- elapsedDelayMs += delay;
630
- }
631
- throw new Error("HubSpot retry budget was exhausted.");
632
- }
633
-
634
- private async schema(objectType: string): Promise<HubSpotSchema | undefined> {
635
- try {
636
- return await this.request<HubSpotSchema>({
637
- path: `/crm/v3/schemas/${encodeURIComponent(objectType)}`,
638
- });
639
- } catch (error) {
640
- if (isNotFound(error)) return undefined;
641
- throw error;
642
- }
643
- }
644
-
645
- private async properties(objectType: string): Promise<HubSpotProperty[]> {
646
- const response = await this.request<{ results?: HubSpotProperty[] }>({
647
- path: `/crm/v3/properties/${encodeURIComponent(objectType)}`,
648
- });
649
- return response.results ?? [];
650
- }
651
-
652
- private async objectDefinition(
653
- objectType: string,
654
- ): Promise<CrmObjectDefinition> {
655
- const [schema, properties] = await Promise.all([
656
- this.schema(objectType),
657
- this.properties(objectType),
658
- ]);
659
- return definitionFrom(
660
- this.workspaceConnection,
661
- objectType,
662
- schema,
663
- properties,
664
- );
665
- }
666
-
667
- async discoverObjects(): Promise<CrmObjectDefinition[]> {
668
- const schemas = await this.request<{ results?: HubSpotSchema[] }>({
669
- path: "/crm/v3/schemas",
670
- });
671
- const objectTypes = uniqueStrings([
672
- ...CORE_OBJECT_TYPES,
673
- ...(schemas.results ?? []).map(
674
- (schema) =>
675
- nonEmpty(schema.name) ??
676
- (schema.objectTypeId == null ? "" : String(schema.objectTypeId)),
677
- ),
678
- ]);
679
- return Promise.all(
680
- objectTypes.map((objectType) => this.objectDefinition(objectType)),
681
- );
682
- }
683
-
684
- async describeObject(objectType: string): Promise<CrmObjectDefinition> {
685
- const normalized = nonEmpty(objectType);
686
- if (!normalized) throw new Error("HubSpot objectType is required.");
687
- return this.objectDefinition(normalized);
688
- }
689
-
690
- getAccessScope(objectType: string): CrmAccessScope {
691
- const normalized = nonEmpty(objectType);
692
- if (!normalized) throw new Error("HubSpot objectType is required.");
693
- return accessScopeFor(this.workspaceConnection, normalized);
694
- }
695
-
696
- private async listPage(input: {
697
- objectType: HubSpotObjectType;
698
- fields: string[];
699
- limit: number;
700
- cursor?: string;
701
- archived?: boolean;
702
- filterGroups?: unknown[];
703
- }): Promise<HubSpotListResponse> {
704
- const objectType = encodeURIComponent(input.objectType);
705
- if (input.filterGroups?.length) {
706
- return this.request<HubSpotListResponse>({
707
- method: "POST",
708
- retrySafe: true,
709
- path: `/crm/v3/objects/${objectType}/search`,
710
- body: {
711
- limit: input.limit,
712
- ...(input.cursor ? { after: input.cursor } : {}),
713
- ...(input.fields.length ? { properties: input.fields } : {}),
714
- filterGroups: input.filterGroups,
715
- },
716
- });
717
- }
718
- return this.request<HubSpotListResponse>({
719
- path: `/crm/v3/objects/${objectType}${toQuery({
720
- limit: String(input.limit),
721
- ...(input.cursor ? { after: input.cursor } : {}),
722
- ...(input.fields.length ? { properties: input.fields.join(",") } : {}),
723
- ...(input.archived ? { archived: "true" } : {}),
724
- })}`,
725
- });
726
- }
727
-
728
- private filtersForScope(scope: {
729
- objectType: string;
730
- pipelineIds?: string[];
731
- ownerIds?: string[];
732
- updatedAfter?: string;
733
- }): unknown[] {
734
- const filters: Array<Record<string, unknown>> = [];
735
- if (
736
- scope.objectType.toLowerCase() === "deals" &&
737
- scope.pipelineIds?.length
738
- ) {
739
- filters.push({
740
- propertyName: "pipeline",
741
- operator: "IN",
742
- values: uniqueStrings(scope.pipelineIds),
743
- });
744
- }
745
- if (scope.ownerIds?.length) {
746
- filters.push({
747
- propertyName: "hubspot_owner_id",
748
- operator: "IN",
749
- values: uniqueStrings(scope.ownerIds),
750
- });
751
- }
752
- if (scope.updatedAfter) {
753
- filters.push({
754
- propertyName:
755
- scope.objectType.toLowerCase() === "contacts"
756
- ? "lastmodifieddate"
757
- : "hs_lastmodifieddate",
758
- operator: "GTE",
759
- value: scope.updatedAfter,
760
- });
761
- }
762
- return filters.length ? [{ filters }] : [];
763
- }
764
-
765
- async syncPage(input: {
766
- scope: {
767
- objectType: string;
768
- pipelineIds?: string[];
769
- ownerIds?: string[];
770
- recordIds?: string[];
771
- associatedRecordIds?: string[];
772
- updatedAfter?: string;
773
- includeDeleted?: boolean;
774
- };
775
- fieldAllowList: string[];
776
- cursor?: string;
777
- limit: number;
778
- }): Promise<CrmSyncPage> {
779
- const objectType = nonEmpty(input.scope.objectType);
780
- if (!objectType)
781
- throw new Error("HubSpot sync scope objectType is required.");
782
- const fields = uniqueStrings(input.fieldAllowList);
783
- const limit = Math.max(1, Math.min(MAX_PAGE_SIZE, input.limit));
784
- const recordIds = uniqueStrings(input.scope.recordIds ?? []);
785
- let page: HubSpotListResponse;
786
- if (recordIds.length) {
787
- const state = decodedCursor(input.cursor);
788
- const offset = Math.min(state.index, recordIds.length);
789
- page = await this.request<HubSpotListResponse>({
790
- method: "POST",
791
- retrySafe: true,
792
- path: `/crm/v3/objects/${encodeURIComponent(objectType)}/batch/read`,
793
- body: {
794
- inputs: recordIds.slice(offset, offset + limit).map((id) => ({ id })),
795
- ...(fields.length ? { properties: fields } : {}),
796
- ...(input.scope.includeDeleted ? { archived: true } : {}),
797
- },
798
- });
799
- } else {
800
- page = await this.listPage({
801
- objectType,
802
- fields,
803
- limit,
804
- cursor: input.cursor,
805
- archived: input.scope.includeDeleted,
806
- filterGroups: this.filtersForScope(input.scope),
807
- });
808
- }
809
- const associatedIds = new Set(
810
- uniqueStrings(input.scope.associatedRecordIds ?? []),
811
- );
812
- const records = (page.results ?? [])
813
- .filter(
814
- (record) => !associatedIds.size || associatedIds.has(String(record.id)),
815
- )
816
- .map((record) =>
817
- projectRecord(this.workspaceConnection, objectType, record, fields),
818
- )
819
- .filter((record): record is CrmRecord => record !== null);
820
- const nextCursor = recordIds.length
821
- ? (() => {
822
- const offset = decodedCursor(input.cursor).index + limit;
823
- return offset < recordIds.length
824
- ? encodeCursorState({ index: offset })
825
- : undefined;
826
- })()
827
- : page.paging?.next?.after;
828
- return {
829
- records,
830
- relationships: [],
831
- ...(nextCursor ? { nextCursor } : {}),
832
- complete: !nextCursor,
833
- };
834
- }
835
-
836
- async getRecord(input: {
837
- record: CrmRecordRef;
838
- fields: string[];
839
- }): Promise<CrmRecord | null> {
840
- assertOwnedRecord(this.workspaceConnection, input.record);
841
- const fields = uniqueStrings(input.fields);
842
- try {
843
- const record = await this.request<HubSpotRecord>({
844
- path: `/crm/v3/objects/${encodeURIComponent(input.record.objectType)}/${encodeURIComponent(input.record.remoteId)}${toQuery(
845
- {
846
- ...(fields.length ? { properties: fields.join(",") } : {}),
847
- },
848
- )}`,
849
- });
850
- return projectRecord(
851
- this.workspaceConnection,
852
- input.record.objectType,
853
- record,
854
- fields,
855
- );
856
- } catch (error) {
857
- if (isNotFound(error)) return null;
858
- throw error;
859
- }
860
- }
861
-
862
- async search(input: {
863
- objectTypes: string[];
864
- query: string;
865
- fields: string[];
866
- limit: number;
867
- cursor?: string;
868
- }): Promise<CrmSyncPage> {
869
- const objectTypes = uniqueStrings(input.objectTypes);
870
- if (!objectTypes.length)
871
- return { records: [], relationships: [], complete: true };
872
- const fields = uniqueStrings(input.fields);
873
- const limit = Math.max(1, Math.min(MAX_PAGE_SIZE, input.limit));
874
- const state = decodedCursor(input.cursor);
875
- const index = Math.min(state.index, objectTypes.length - 1);
876
- const objectType = objectTypes[index]!;
877
- const page = await this.request<HubSpotListResponse>({
878
- method: "POST",
879
- path: `/crm/v3/objects/${encodeURIComponent(objectType)}/search`,
880
- body: {
881
- query: input.query,
882
- limit,
883
- ...(state.after ? { after: state.after } : {}),
884
- ...(fields.length ? { properties: fields } : {}),
885
- },
886
- });
887
- const records = (page.results ?? [])
888
- .map((record) =>
889
- projectRecord(this.workspaceConnection, objectType, record, fields),
890
- )
891
- .filter((record): record is CrmRecord => record !== null);
892
- const after = page.paging?.next?.after;
893
- const nextCursor = after
894
- ? encodedCursor(index, after)
895
- : index + 1 < objectTypes.length
896
- ? encodeCursorState({ index: index + 1 })
897
- : undefined;
898
- return {
899
- records,
900
- relationships: [],
901
- ...(nextCursor ? { nextCursor } : {}),
902
- complete: !nextCursor,
903
- };
904
- }
905
-
906
- async listRelationships(input: {
907
- record: CrmRecordRef;
908
- targetObjectTypes?: string[];
909
- limit: number;
910
- cursor?: string;
911
- }): Promise<{
912
- relationships: CrmRelationship[];
913
- nextCursor?: string;
914
- complete: boolean;
915
- }> {
916
- assertOwnedRecord(this.workspaceConnection, input.record);
917
- const targetObjectTypes = uniqueStrings(
918
- input.targetObjectTypes?.length
919
- ? input.targetObjectTypes
920
- : CORE_OBJECT_TYPES,
921
- ).filter((objectType) => objectType !== input.record.objectType);
922
- if (!targetObjectTypes.length) {
923
- return { relationships: [], complete: true };
924
- }
925
- const state = decodedCursor(input.cursor);
926
- const index = Math.min(state.index, targetObjectTypes.length - 1);
927
- const targetObjectType = targetObjectTypes[index]!;
928
- const response = await this.request<HubSpotAssociationResponse>({
929
- path: `/crm/v4/objects/${encodeURIComponent(input.record.objectType)}/${encodeURIComponent(input.record.remoteId)}/associations/${encodeURIComponent(targetObjectType)}${toQuery(
930
- {
931
- limit: String(Math.max(1, Math.min(MAX_PAGE_SIZE, input.limit))),
932
- ...(state.after ? { after: state.after } : {}),
933
- },
934
- )}`,
935
- });
936
- const relationships: CrmRelationship[] = [];
937
- for (const association of response.results ?? []) {
938
- const remoteId = association.toObjectId ?? association.id;
939
- if (remoteId == null) continue;
940
- const types = association.associationTypes?.length
941
- ? association.associationTypes
942
- : [
943
- {
944
- associationCategory: "HUBSPOT_DEFINED",
945
- associationTypeId: "default",
946
- },
947
- ];
948
- for (const type of types) {
949
- const relationshipType = [
950
- type.associationCategory ?? "HUBSPOT_DEFINED",
951
- type.associationTypeId == null
952
- ? "default"
953
- : String(type.associationTypeId),
954
- ].join(":");
955
- relationships.push({
956
- from: input.record,
957
- to: recordRef(
958
- this.workspaceConnection,
959
- targetObjectType,
960
- String(remoteId),
961
- ),
962
- relationshipType,
963
- ...(type.label ? { label: type.label } : {}),
964
- });
965
- }
966
- }
967
- const after = response.paging?.next?.after;
968
- const nextCursor = after
969
- ? encodedCursor(index, after)
970
- : index + 1 < targetObjectTypes.length
971
- ? encodeCursorState({ index: index + 1 })
972
- : undefined;
973
- return {
974
- relationships,
975
- ...(nextCursor ? { nextCursor } : {}),
976
- complete: !nextCursor,
977
- };
978
- }
979
-
980
- async applyMutation(mutation: Parameters<CrmAdapter["applyMutation"]>[0]) {
981
- assertOwnedRecord(this.workspaceConnection, mutation.record);
982
- if (!mutation.idempotencyKey.trim()) {
983
- return {
984
- status: "rejected" as const,
985
- message: "An idempotency key is required.",
986
- };
987
- }
988
- if (mutation.operation === "delete") {
989
- return {
990
- status: "rejected" as const,
991
- message:
992
- "HubSpot deletion is disabled during the phase-1 CRM transport rollout.",
993
- };
994
- }
995
- if (
996
- mutation.operation === "associate" ||
997
- mutation.operation === "disassociate"
998
- ) {
999
- return {
1000
- status: "rejected" as const,
1001
- message:
1002
- "HubSpot association mutation requires an explicit association definition and is not enabled in phase 1.",
1003
- };
1004
- }
1005
- if (
1006
- !scopeAllows(
1007
- this.workspaceConnection,
1008
- mutation.record.objectType,
1009
- "write",
1010
- )
1011
- ) {
1012
- return {
1013
- status: "rejected" as const,
1014
- message:
1015
- "The workspace connection does not prove write access for this HubSpot object.",
1016
- };
1017
- }
1018
- const fields = Object.entries(mutation.fields ?? {}).reduce<
1019
- Record<string, CrmValue>
1020
- >((result, [key, value]) => {
1021
- if (key.trim()) result[key] = value;
1022
- return result;
1023
- }, {});
1024
- if (!Object.keys(fields).length) {
1025
- return {
1026
- status: "rejected" as const,
1027
- message: "At least one field is required.",
1028
- };
1029
- }
1030
- if (mutation.operation === "update" && !mutation.expectedRemoteRevision) {
1031
- return {
1032
- status: "rejected" as const,
1033
- message: "HubSpot updates require a current remote revision.",
1034
- };
1035
- }
1036
- if (mutation.expectedRemoteRevision) {
1037
- const current = await this.getRecord({
1038
- record: mutation.record,
1039
- fields: [],
1040
- });
1041
- if (
1042
- !current ||
1043
- current.remoteRevision !== mutation.expectedRemoteRevision
1044
- ) {
1045
- return {
1046
- status: "conflict" as const,
1047
- ...(current?.remoteRevision
1048
- ? { remoteRevision: current.remoteRevision }
1049
- : {}),
1050
- message:
1051
- "The HubSpot record changed before this mutation could be applied.",
1052
- };
1053
- }
1054
- if (!this.capabilities.conditionalMutations) {
1055
- return {
1056
- status: "rejected" as const,
1057
- remoteRevision: current.remoteRevision,
1058
- message:
1059
- "HubSpot does not support an atomic conditional update for this record.",
1060
- };
1061
- }
1062
- }
1063
- try {
1064
- const objectType = encodeURIComponent(mutation.record.objectType);
1065
- const response = await this.request<HubSpotRecord>({
1066
- method: mutation.operation === "create" ? "POST" : "PATCH",
1067
- path:
1068
- mutation.operation === "create"
1069
- ? `/crm/v3/objects/${objectType}`
1070
- : `/crm/v3/objects/${objectType}/${encodeURIComponent(mutation.record.remoteId)}`,
1071
- body: { properties: fields },
1072
- headers: { "X-Idempotency-Key": mutation.idempotencyKey },
1073
- });
1074
- const record = projectRecord(
1075
- this.workspaceConnection,
1076
- mutation.record.objectType,
1077
- response,
1078
- Object.keys(fields),
1079
- );
1080
- if (!record) {
1081
- return {
1082
- status: "rejected" as const,
1083
- message: "HubSpot returned no record identity.",
1084
- };
1085
- }
1086
- return {
1087
- status: "applied" as const,
1088
- record,
1089
- ...(record.remoteRevision
1090
- ? { remoteRevision: record.remoteRevision }
1091
- : {}),
1092
- };
1093
- } catch (error) {
1094
- return {
1095
- status: "rejected" as const,
1096
- message:
1097
- error instanceof Error ? error.message : "HubSpot mutation failed.",
1098
- };
1099
- }
1100
- }
1101
- }
1102
-
1103
- function requireResolvedConnection(
1104
- result: ResolvedWorkspaceConnectionForApp,
1105
- ): WorkspaceConnectionForApp {
1106
- if (!result.available || !result.connection) {
1107
- throw new Error(
1108
- `HubSpot workspace connection is unavailable: ${result.reason}`,
1109
- );
1110
- }
1111
- return result.connection;
1112
- }
1113
-
1114
- export async function createHubSpotCrmAdapter(
1115
- options: CreateHubSpotCrmAdapterOptions = {},
1116
- ): Promise<HubSpotCrmAdapter> {
1117
- const resolved = await resolveWorkspaceConnectionForApp({
1118
- appId: CRM_APP_ID,
1119
- provider: "hubspot",
1120
- ...(options.connectionId ? { connectionId: options.connectionId } : {}),
1121
- requireConnected: true,
1122
- });
1123
- const connection = requireResolvedConnection(resolved);
1124
- if (options.transport) {
1125
- return new HubSpotCrmAdapter({ connection, transport: options.transport });
1126
- }
1127
- for (const key of HUBSPOT_CREDENTIAL_KEYS) {
1128
- const credential = await resolveWorkspaceConnectionCredentialForApp({
1129
- appId: CRM_APP_ID,
1130
- provider: "hubspot",
1131
- key,
1132
- connectionId: connection.id,
1133
- ...(options.userEmail ? { userEmail: options.userEmail } : {}),
1134
- ...(options.orgId !== undefined ? { orgId: options.orgId } : {}),
1135
- });
1136
- if (!credential.available || !credential.value) continue;
1137
- if (credential.provenance?.connectionId !== connection.id) continue;
1138
- return new HubSpotCrmAdapter({
1139
- connection,
1140
- transport: new FetchHubSpotTransport(credential.value),
1141
- });
1142
- }
1143
- throw new Error(
1144
- "HubSpot workspace connection is unavailable: no scoped credential was resolved for the granted connection.",
1145
- );
1146
- }