@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,1143 @@
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: "hs_lastmodifieddate",
755
+ operator: "GTE",
756
+ value: scope.updatedAfter,
757
+ });
758
+ }
759
+ return filters.length ? [{ filters }] : [];
760
+ }
761
+
762
+ async syncPage(input: {
763
+ scope: {
764
+ objectType: string;
765
+ pipelineIds?: string[];
766
+ ownerIds?: string[];
767
+ recordIds?: string[];
768
+ associatedRecordIds?: string[];
769
+ updatedAfter?: string;
770
+ includeDeleted?: boolean;
771
+ };
772
+ fieldAllowList: string[];
773
+ cursor?: string;
774
+ limit: number;
775
+ }): Promise<CrmSyncPage> {
776
+ const objectType = nonEmpty(input.scope.objectType);
777
+ if (!objectType)
778
+ throw new Error("HubSpot sync scope objectType is required.");
779
+ const fields = uniqueStrings(input.fieldAllowList);
780
+ const limit = Math.max(1, Math.min(MAX_PAGE_SIZE, input.limit));
781
+ const recordIds = uniqueStrings(input.scope.recordIds ?? []);
782
+ let page: HubSpotListResponse;
783
+ if (recordIds.length) {
784
+ const state = decodedCursor(input.cursor);
785
+ const offset = Math.min(state.index, recordIds.length);
786
+ page = await this.request<HubSpotListResponse>({
787
+ method: "POST",
788
+ retrySafe: true,
789
+ path: `/crm/v3/objects/${encodeURIComponent(objectType)}/batch/read`,
790
+ body: {
791
+ inputs: recordIds.slice(offset, offset + limit).map((id) => ({ id })),
792
+ ...(fields.length ? { properties: fields } : {}),
793
+ ...(input.scope.includeDeleted ? { archived: true } : {}),
794
+ },
795
+ });
796
+ } else {
797
+ page = await this.listPage({
798
+ objectType,
799
+ fields,
800
+ limit,
801
+ cursor: input.cursor,
802
+ archived: input.scope.includeDeleted,
803
+ filterGroups: this.filtersForScope(input.scope),
804
+ });
805
+ }
806
+ const associatedIds = new Set(
807
+ uniqueStrings(input.scope.associatedRecordIds ?? []),
808
+ );
809
+ const records = (page.results ?? [])
810
+ .filter(
811
+ (record) => !associatedIds.size || associatedIds.has(String(record.id)),
812
+ )
813
+ .map((record) =>
814
+ projectRecord(this.workspaceConnection, objectType, record, fields),
815
+ )
816
+ .filter((record): record is CrmRecord => record !== null);
817
+ const nextCursor = recordIds.length
818
+ ? (() => {
819
+ const offset = decodedCursor(input.cursor).index + limit;
820
+ return offset < recordIds.length
821
+ ? encodeCursorState({ index: offset })
822
+ : undefined;
823
+ })()
824
+ : page.paging?.next?.after;
825
+ return {
826
+ records,
827
+ relationships: [],
828
+ ...(nextCursor ? { nextCursor } : {}),
829
+ complete: !nextCursor,
830
+ };
831
+ }
832
+
833
+ async getRecord(input: {
834
+ record: CrmRecordRef;
835
+ fields: string[];
836
+ }): Promise<CrmRecord | null> {
837
+ assertOwnedRecord(this.workspaceConnection, input.record);
838
+ const fields = uniqueStrings(input.fields);
839
+ try {
840
+ const record = await this.request<HubSpotRecord>({
841
+ path: `/crm/v3/objects/${encodeURIComponent(input.record.objectType)}/${encodeURIComponent(input.record.remoteId)}${toQuery(
842
+ {
843
+ ...(fields.length ? { properties: fields.join(",") } : {}),
844
+ },
845
+ )}`,
846
+ });
847
+ return projectRecord(
848
+ this.workspaceConnection,
849
+ input.record.objectType,
850
+ record,
851
+ fields,
852
+ );
853
+ } catch (error) {
854
+ if (isNotFound(error)) return null;
855
+ throw error;
856
+ }
857
+ }
858
+
859
+ async search(input: {
860
+ objectTypes: string[];
861
+ query: string;
862
+ fields: string[];
863
+ limit: number;
864
+ cursor?: string;
865
+ }): Promise<CrmSyncPage> {
866
+ const objectTypes = uniqueStrings(input.objectTypes);
867
+ if (!objectTypes.length)
868
+ return { records: [], relationships: [], complete: true };
869
+ const fields = uniqueStrings(input.fields);
870
+ const limit = Math.max(1, Math.min(MAX_PAGE_SIZE, input.limit));
871
+ const state = decodedCursor(input.cursor);
872
+ const index = Math.min(state.index, objectTypes.length - 1);
873
+ const objectType = objectTypes[index]!;
874
+ const page = await this.request<HubSpotListResponse>({
875
+ method: "POST",
876
+ path: `/crm/v3/objects/${encodeURIComponent(objectType)}/search`,
877
+ body: {
878
+ query: input.query,
879
+ limit,
880
+ ...(state.after ? { after: state.after } : {}),
881
+ ...(fields.length ? { properties: fields } : {}),
882
+ },
883
+ });
884
+ const records = (page.results ?? [])
885
+ .map((record) =>
886
+ projectRecord(this.workspaceConnection, objectType, record, fields),
887
+ )
888
+ .filter((record): record is CrmRecord => record !== null);
889
+ const after = page.paging?.next?.after;
890
+ const nextCursor = after
891
+ ? encodedCursor(index, after)
892
+ : index + 1 < objectTypes.length
893
+ ? encodeCursorState({ index: index + 1 })
894
+ : undefined;
895
+ return {
896
+ records,
897
+ relationships: [],
898
+ ...(nextCursor ? { nextCursor } : {}),
899
+ complete: !nextCursor,
900
+ };
901
+ }
902
+
903
+ async listRelationships(input: {
904
+ record: CrmRecordRef;
905
+ targetObjectTypes?: string[];
906
+ limit: number;
907
+ cursor?: string;
908
+ }): Promise<{
909
+ relationships: CrmRelationship[];
910
+ nextCursor?: string;
911
+ complete: boolean;
912
+ }> {
913
+ assertOwnedRecord(this.workspaceConnection, input.record);
914
+ const targetObjectTypes = uniqueStrings(
915
+ input.targetObjectTypes?.length
916
+ ? input.targetObjectTypes
917
+ : CORE_OBJECT_TYPES,
918
+ ).filter((objectType) => objectType !== input.record.objectType);
919
+ if (!targetObjectTypes.length) {
920
+ return { relationships: [], complete: true };
921
+ }
922
+ const state = decodedCursor(input.cursor);
923
+ const index = Math.min(state.index, targetObjectTypes.length - 1);
924
+ const targetObjectType = targetObjectTypes[index]!;
925
+ const response = await this.request<HubSpotAssociationResponse>({
926
+ path: `/crm/v4/objects/${encodeURIComponent(input.record.objectType)}/${encodeURIComponent(input.record.remoteId)}/associations/${encodeURIComponent(targetObjectType)}${toQuery(
927
+ {
928
+ limit: String(Math.max(1, Math.min(MAX_PAGE_SIZE, input.limit))),
929
+ ...(state.after ? { after: state.after } : {}),
930
+ },
931
+ )}`,
932
+ });
933
+ const relationships: CrmRelationship[] = [];
934
+ for (const association of response.results ?? []) {
935
+ const remoteId = association.toObjectId ?? association.id;
936
+ if (remoteId == null) continue;
937
+ const types = association.associationTypes?.length
938
+ ? association.associationTypes
939
+ : [
940
+ {
941
+ associationCategory: "HUBSPOT_DEFINED",
942
+ associationTypeId: "default",
943
+ },
944
+ ];
945
+ for (const type of types) {
946
+ const relationshipType = [
947
+ type.associationCategory ?? "HUBSPOT_DEFINED",
948
+ type.associationTypeId == null
949
+ ? "default"
950
+ : String(type.associationTypeId),
951
+ ].join(":");
952
+ relationships.push({
953
+ from: input.record,
954
+ to: recordRef(
955
+ this.workspaceConnection,
956
+ targetObjectType,
957
+ String(remoteId),
958
+ ),
959
+ relationshipType,
960
+ ...(type.label ? { label: type.label } : {}),
961
+ });
962
+ }
963
+ }
964
+ const after = response.paging?.next?.after;
965
+ const nextCursor = after
966
+ ? encodedCursor(index, after)
967
+ : index + 1 < targetObjectTypes.length
968
+ ? encodeCursorState({ index: index + 1 })
969
+ : undefined;
970
+ return {
971
+ relationships,
972
+ ...(nextCursor ? { nextCursor } : {}),
973
+ complete: !nextCursor,
974
+ };
975
+ }
976
+
977
+ async applyMutation(mutation: Parameters<CrmAdapter["applyMutation"]>[0]) {
978
+ assertOwnedRecord(this.workspaceConnection, mutation.record);
979
+ if (!mutation.idempotencyKey.trim()) {
980
+ return {
981
+ status: "rejected" as const,
982
+ message: "An idempotency key is required.",
983
+ };
984
+ }
985
+ if (mutation.operation === "delete") {
986
+ return {
987
+ status: "rejected" as const,
988
+ message:
989
+ "HubSpot deletion is disabled during the phase-1 CRM transport rollout.",
990
+ };
991
+ }
992
+ if (
993
+ mutation.operation === "associate" ||
994
+ mutation.operation === "disassociate"
995
+ ) {
996
+ return {
997
+ status: "rejected" as const,
998
+ message:
999
+ "HubSpot association mutation requires an explicit association definition and is not enabled in phase 1.",
1000
+ };
1001
+ }
1002
+ if (
1003
+ !scopeAllows(
1004
+ this.workspaceConnection,
1005
+ mutation.record.objectType,
1006
+ "write",
1007
+ )
1008
+ ) {
1009
+ return {
1010
+ status: "rejected" as const,
1011
+ message:
1012
+ "The workspace connection does not prove write access for this HubSpot object.",
1013
+ };
1014
+ }
1015
+ const fields = Object.entries(mutation.fields ?? {}).reduce<
1016
+ Record<string, CrmValue>
1017
+ >((result, [key, value]) => {
1018
+ if (key.trim()) result[key] = value;
1019
+ return result;
1020
+ }, {});
1021
+ if (!Object.keys(fields).length) {
1022
+ return {
1023
+ status: "rejected" as const,
1024
+ message: "At least one field is required.",
1025
+ };
1026
+ }
1027
+ if (mutation.operation === "update" && !mutation.expectedRemoteRevision) {
1028
+ return {
1029
+ status: "rejected" as const,
1030
+ message: "HubSpot updates require a current remote revision.",
1031
+ };
1032
+ }
1033
+ if (mutation.expectedRemoteRevision) {
1034
+ const current = await this.getRecord({
1035
+ record: mutation.record,
1036
+ fields: [],
1037
+ });
1038
+ if (
1039
+ !current ||
1040
+ current.remoteRevision !== mutation.expectedRemoteRevision
1041
+ ) {
1042
+ return {
1043
+ status: "conflict" as const,
1044
+ ...(current?.remoteRevision
1045
+ ? { remoteRevision: current.remoteRevision }
1046
+ : {}),
1047
+ message:
1048
+ "The HubSpot record changed before this mutation could be applied.",
1049
+ };
1050
+ }
1051
+ if (!this.capabilities.conditionalMutations) {
1052
+ return {
1053
+ status: "rejected" as const,
1054
+ remoteRevision: current.remoteRevision,
1055
+ message:
1056
+ "HubSpot does not support an atomic conditional update for this record.",
1057
+ };
1058
+ }
1059
+ }
1060
+ try {
1061
+ const objectType = encodeURIComponent(mutation.record.objectType);
1062
+ const response = await this.request<HubSpotRecord>({
1063
+ method: mutation.operation === "create" ? "POST" : "PATCH",
1064
+ path:
1065
+ mutation.operation === "create"
1066
+ ? `/crm/v3/objects/${objectType}`
1067
+ : `/crm/v3/objects/${objectType}/${encodeURIComponent(mutation.record.remoteId)}`,
1068
+ body: { properties: fields },
1069
+ headers: { "X-Idempotency-Key": mutation.idempotencyKey },
1070
+ });
1071
+ const record = projectRecord(
1072
+ this.workspaceConnection,
1073
+ mutation.record.objectType,
1074
+ response,
1075
+ Object.keys(fields),
1076
+ );
1077
+ if (!record) {
1078
+ return {
1079
+ status: "rejected" as const,
1080
+ message: "HubSpot returned no record identity.",
1081
+ };
1082
+ }
1083
+ return {
1084
+ status: "applied" as const,
1085
+ record,
1086
+ ...(record.remoteRevision
1087
+ ? { remoteRevision: record.remoteRevision }
1088
+ : {}),
1089
+ };
1090
+ } catch (error) {
1091
+ return {
1092
+ status: "rejected" as const,
1093
+ message:
1094
+ error instanceof Error ? error.message : "HubSpot mutation failed.",
1095
+ };
1096
+ }
1097
+ }
1098
+ }
1099
+
1100
+ function requireResolvedConnection(
1101
+ result: ResolvedWorkspaceConnectionForApp,
1102
+ ): WorkspaceConnectionForApp {
1103
+ if (!result.available || !result.connection) {
1104
+ throw new Error(
1105
+ `HubSpot workspace connection is unavailable: ${result.reason}`,
1106
+ );
1107
+ }
1108
+ return result.connection;
1109
+ }
1110
+
1111
+ export async function createHubSpotCrmAdapter(
1112
+ options: CreateHubSpotCrmAdapterOptions = {},
1113
+ ): Promise<HubSpotCrmAdapter> {
1114
+ const resolved = await resolveWorkspaceConnectionForApp({
1115
+ appId: CRM_APP_ID,
1116
+ provider: "hubspot",
1117
+ ...(options.connectionId ? { connectionId: options.connectionId } : {}),
1118
+ requireConnected: true,
1119
+ });
1120
+ const connection = requireResolvedConnection(resolved);
1121
+ if (options.transport) {
1122
+ return new HubSpotCrmAdapter({ connection, transport: options.transport });
1123
+ }
1124
+ for (const key of HUBSPOT_CREDENTIAL_KEYS) {
1125
+ const credential = await resolveWorkspaceConnectionCredentialForApp({
1126
+ appId: CRM_APP_ID,
1127
+ provider: "hubspot",
1128
+ key,
1129
+ connectionId: connection.id,
1130
+ ...(options.userEmail ? { userEmail: options.userEmail } : {}),
1131
+ ...(options.orgId !== undefined ? { orgId: options.orgId } : {}),
1132
+ });
1133
+ if (!credential.available || !credential.value) continue;
1134
+ if (credential.provenance?.connectionId !== connection.id) continue;
1135
+ return new HubSpotCrmAdapter({
1136
+ connection,
1137
+ transport: new FetchHubSpotTransport(credential.value),
1138
+ });
1139
+ }
1140
+ throw new Error(
1141
+ "HubSpot workspace connection is unavailable: no scoped credential was resolved for the granted connection.",
1142
+ );
1143
+ }