@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,1170 @@
1
+ import { accessFilter, assertAccess } from "@agent-native/core/sharing";
2
+ import { and, asc, desc, eq, exists, inArray, isNull, like } from "drizzle-orm";
3
+
4
+ import type {
5
+ CrmAccessScope,
6
+ CrmRelationship,
7
+ CrmValue,
8
+ } from "../../shared/crm-contract.js";
9
+ import {
10
+ createConnectedCrmAdapter,
11
+ isConnectedCrmProvider,
12
+ } from "../crm/adapter.js";
13
+ import {
14
+ isBoundedCrmValue,
15
+ isSafeCrmMutationFieldName,
16
+ } from "../crm/crm-field-firewall.js";
17
+ import { resolveNativeCrmAccessScope } from "../crm/native-adapter.js";
18
+ import {
19
+ parseCrmAccessScope,
20
+ relatedSummaries,
21
+ scopesAreCompatible,
22
+ type RelatedRecordSummary,
23
+ } from "../crm/read-through.js";
24
+ import { getDb, schema } from "./index.js";
25
+
26
+ const MAX_RECORD_LIMIT = 100;
27
+ const RECORD_DETAIL_LIMIT = 20;
28
+ const MAX_SCOPE_VALIDATIONS = 20;
29
+ const SAFE_VIEW_COLUMNS = new Set([
30
+ "displayName",
31
+ "primaryEmail",
32
+ "domain",
33
+ "stage",
34
+ "ownerName",
35
+ "nextContactAt",
36
+ "remoteUpdatedAt",
37
+ "updatedAt",
38
+ ]);
39
+ const SAFE_VIEW_SORTS = new Set([
40
+ "displayName",
41
+ "stage",
42
+ "ownerName",
43
+ "nextContactAt",
44
+ "remoteUpdatedAt",
45
+ "updatedAt",
46
+ ]);
47
+
48
+ export type CrmListKind =
49
+ | "account"
50
+ | "person"
51
+ | "opportunity"
52
+ | "activity"
53
+ | "task"
54
+ | "custom";
55
+
56
+ type Primitive = string | number | boolean | null;
57
+
58
+ type SavedViewConfig = {
59
+ id: string;
60
+ name: string;
61
+ kind?: CrmListKind;
62
+ dataProgramId?: string;
63
+ query?: string;
64
+ fieldEquals: Record<string, Primitive>;
65
+ columns: string[];
66
+ sort: Array<{ field: string; direction: "asc" | "desc" }>;
67
+ };
68
+
69
+ type ScopeValidationTarget = {
70
+ connectionId: string;
71
+ workspaceConnectionId: string | null;
72
+ provider: string;
73
+ objectType: string;
74
+ };
75
+
76
+ export type CrmScopeResolver = (
77
+ target: ScopeValidationTarget,
78
+ ) => Promise<CrmAccessScope | null>;
79
+
80
+ function decodeCursor(cursor: string | undefined): number {
81
+ if (!cursor) return 0;
82
+ const value = Number.parseInt(cursor, 10);
83
+ return Number.isSafeInteger(value) && value >= 0 ? value : 0;
84
+ }
85
+
86
+ function page<T>(rows: T[], limit: number) {
87
+ return {
88
+ rows: rows.slice(0, limit),
89
+ nextCursor:
90
+ rows.length > limit ? String(decodeCursor(undefined) + limit) : undefined,
91
+ };
92
+ }
93
+
94
+ function toRecordSummary(
95
+ row: {
96
+ id: string;
97
+ displayName: string;
98
+ kind: string;
99
+ primaryEmail: string | null;
100
+ domain: string | null;
101
+ stage: string | null;
102
+ ownerName: string | null;
103
+ nextContactAt: string | null;
104
+ remoteUpdatedAt: string | null;
105
+ updatedAt: string;
106
+ },
107
+ columns?: string[],
108
+ ) {
109
+ const included = new Set(columns ?? SAFE_VIEW_COLUMNS);
110
+ return {
111
+ id: row.id,
112
+ displayName: row.displayName,
113
+ kind: row.kind,
114
+ ...(included.has("domain") || included.has("primaryEmail")
115
+ ? { subtitle: row.domain ?? row.primaryEmail ?? undefined }
116
+ : {}),
117
+ ...(included.has("ownerName") ? { owner: row.ownerName ?? undefined } : {}),
118
+ ...(included.has("stage") ? { stage: row.stage ?? undefined } : {}),
119
+ ...(included.has("nextContactAt")
120
+ ? { nextStep: row.nextContactAt ?? undefined }
121
+ : {}),
122
+ ...(included.has("remoteUpdatedAt") || included.has("updatedAt")
123
+ ? { updatedAt: row.remoteUpdatedAt ?? row.updatedAt }
124
+ : {}),
125
+ };
126
+ }
127
+
128
+ function parseStoredValue(row: {
129
+ valueType: string;
130
+ stringValue: string | null;
131
+ numberValue: number | null;
132
+ booleanValue: boolean | null;
133
+ jsonValue: string | null;
134
+ }): unknown {
135
+ if (row.booleanValue !== null) return row.booleanValue;
136
+ if (row.numberValue !== null) return row.numberValue;
137
+ if (row.stringValue !== null) return row.stringValue;
138
+ if (!row.jsonValue) return null;
139
+ try {
140
+ return JSON.parse(row.jsonValue) as unknown;
141
+ } catch {
142
+ return null;
143
+ }
144
+ }
145
+
146
+ function parsePrimitive(value: unknown): value is Primitive {
147
+ return (
148
+ value === null ||
149
+ typeof value === "boolean" ||
150
+ (typeof value === "number" && Number.isFinite(value)) ||
151
+ (typeof value === "string" && value.length <= 2_000)
152
+ );
153
+ }
154
+
155
+ function parseJsonObject(value: string): Record<string, unknown> | null {
156
+ try {
157
+ const parsed = JSON.parse(value) as unknown;
158
+ return parsed && typeof parsed === "object" && !Array.isArray(parsed)
159
+ ? (parsed as Record<string, unknown>)
160
+ : null;
161
+ } catch {
162
+ return null;
163
+ }
164
+ }
165
+
166
+ function parseSavedViewConfig(input: {
167
+ id: string;
168
+ name: string;
169
+ kind: string | null;
170
+ filtersJson: string;
171
+ columnsJson: string;
172
+ sortJson: string;
173
+ dataProgramId: string | null;
174
+ }): SavedViewConfig {
175
+ if (
176
+ input.kind !== null &&
177
+ input.kind !== "account" &&
178
+ input.kind !== "person" &&
179
+ input.kind !== "opportunity"
180
+ ) {
181
+ throw new Error("CRM saved view has an unsupported record kind.");
182
+ }
183
+ const filters = parseJsonObject(input.filtersJson);
184
+ if (!filters) throw new Error("CRM saved view filters are invalid.");
185
+ const filterKeys = Object.keys(filters);
186
+ if (filterKeys.some((key) => key !== "query" && key !== "fieldEquals")) {
187
+ throw new Error("CRM saved view contains unsupported filters.");
188
+ }
189
+ const query = filters.query;
190
+ if (
191
+ query !== undefined &&
192
+ (typeof query !== "string" || query.length > 120)
193
+ ) {
194
+ throw new Error("CRM saved view query is invalid.");
195
+ }
196
+ const rawFieldEquals = filters.fieldEquals;
197
+ if (
198
+ rawFieldEquals !== undefined &&
199
+ (!rawFieldEquals ||
200
+ typeof rawFieldEquals !== "object" ||
201
+ Array.isArray(rawFieldEquals))
202
+ ) {
203
+ throw new Error("CRM saved view field filters are invalid.");
204
+ }
205
+ const fieldEquals = Object.entries(
206
+ (rawFieldEquals ?? {}) as Record<string, unknown>,
207
+ );
208
+ if (
209
+ fieldEquals.length > 12 ||
210
+ fieldEquals.some(
211
+ ([fieldName, value]) =>
212
+ !fieldName.trim() || fieldName.length > 120 || !parsePrimitive(value),
213
+ )
214
+ ) {
215
+ throw new Error("CRM saved view field filters must be bounded primitives.");
216
+ }
217
+ const columns = JSON.parse(input.columnsJson) as unknown;
218
+ if (
219
+ !Array.isArray(columns) ||
220
+ columns.length > 20 ||
221
+ columns.some(
222
+ (column) => typeof column !== "string" || !SAFE_VIEW_COLUMNS.has(column),
223
+ )
224
+ ) {
225
+ throw new Error("CRM saved view columns are unsupported.");
226
+ }
227
+ const sort = JSON.parse(input.sortJson) as unknown;
228
+ if (
229
+ !Array.isArray(sort) ||
230
+ sort.length > 2 ||
231
+ sort.some(
232
+ (entry) =>
233
+ !entry ||
234
+ typeof entry !== "object" ||
235
+ !SAFE_VIEW_SORTS.has((entry as { field?: string }).field ?? "") ||
236
+ ((entry as { direction?: string }).direction !== "asc" &&
237
+ (entry as { direction?: string }).direction !== "desc"),
238
+ )
239
+ ) {
240
+ throw new Error("CRM saved view sort is unsupported.");
241
+ }
242
+ return {
243
+ id: input.id,
244
+ name: input.name,
245
+ ...(input.kind ? { kind: input.kind } : {}),
246
+ ...(input.dataProgramId ? { dataProgramId: input.dataProgramId } : {}),
247
+ ...(query ? { query } : {}),
248
+ fieldEquals: Object.fromEntries(fieldEquals) as Record<string, Primitive>,
249
+ columns,
250
+ sort: sort as Array<{ field: string; direction: "asc" | "desc" }>,
251
+ };
252
+ }
253
+
254
+ function fieldEqualsCondition(fieldName: string, value: Primitive) {
255
+ const scalarCondition =
256
+ value === null
257
+ ? and(
258
+ isNull(schema.crmRecordFields.stringValue),
259
+ isNull(schema.crmRecordFields.numberValue),
260
+ isNull(schema.crmRecordFields.booleanValue),
261
+ isNull(schema.crmRecordFields.jsonValue),
262
+ )
263
+ : typeof value === "string"
264
+ ? eq(schema.crmRecordFields.stringValue, value)
265
+ : typeof value === "number"
266
+ ? eq(schema.crmRecordFields.numberValue, value)
267
+ : eq(schema.crmRecordFields.booleanValue, value);
268
+ return exists(
269
+ getDb()
270
+ .select({ id: schema.crmRecordFields.id })
271
+ .from(schema.crmRecordFields)
272
+ .where(
273
+ and(
274
+ eq(schema.crmRecordFields.recordId, schema.crmRecords.id),
275
+ eq(schema.crmRecordFields.fieldName, fieldName),
276
+ inArray(schema.crmRecordFields.storagePolicy, [
277
+ "mirrored",
278
+ "derived-local",
279
+ "local-authoritative",
280
+ ]),
281
+ accessFilter(schema.crmRecordFields, schema.crmRecordFieldShares),
282
+ scalarCondition,
283
+ ),
284
+ ),
285
+ );
286
+ }
287
+
288
+ function orderForSavedView(view: SavedViewConfig | undefined) {
289
+ const sort = view?.sort[0];
290
+ const column =
291
+ sort?.field === "displayName"
292
+ ? schema.crmRecords.displayName
293
+ : sort?.field === "stage"
294
+ ? schema.crmRecords.stage
295
+ : sort?.field === "ownerName"
296
+ ? schema.crmRecords.ownerName
297
+ : sort?.field === "nextContactAt"
298
+ ? schema.crmRecords.nextContactAt
299
+ : sort?.field === "updatedAt"
300
+ ? schema.crmRecords.updatedAt
301
+ : schema.crmRecords.remoteUpdatedAt;
302
+ const direction = sort?.direction === "asc" ? asc : desc;
303
+ return [direction(column), desc(schema.crmRecords.id)];
304
+ }
305
+
306
+ async function defaultScopeResolver(
307
+ target: ScopeValidationTarget,
308
+ ): Promise<CrmAccessScope | null> {
309
+ if (target.provider === "native") {
310
+ return resolveNativeCrmAccessScope({
311
+ connectionId: target.connectionId,
312
+ objectType: target.objectType,
313
+ });
314
+ }
315
+ if (!isConnectedCrmProvider(target.provider) || !target.workspaceConnectionId)
316
+ return null;
317
+ const adapter = await createConnectedCrmAdapter({
318
+ provider: target.provider,
319
+ connectionId: target.workspaceConnectionId,
320
+ });
321
+ return adapter.getAccessScope(target.objectType);
322
+ }
323
+
324
+ export async function listCrmRecords(
325
+ input: {
326
+ kind?: CrmListKind;
327
+ connectionId?: string;
328
+ query?: string;
329
+ viewId?: string;
330
+ limit: number;
331
+ cursor?: string;
332
+ },
333
+ options: { resolveScope?: CrmScopeResolver } = {},
334
+ ) {
335
+ const db = getDb();
336
+ const offset = decodeCursor(input.cursor);
337
+ const limit = Math.min(input.limit, MAX_RECORD_LIMIT);
338
+ const viewRow = input.viewId
339
+ ? await db
340
+ .select({
341
+ id: schema.crmSavedViews.id,
342
+ name: schema.crmSavedViews.name,
343
+ kind: schema.crmSavedViews.kind,
344
+ filtersJson: schema.crmSavedViews.filtersJson,
345
+ columnsJson: schema.crmSavedViews.columnsJson,
346
+ sortJson: schema.crmSavedViews.sortJson,
347
+ dataProgramId: schema.crmSavedViews.dataProgramId,
348
+ })
349
+ .from(schema.crmSavedViews)
350
+ .where(
351
+ and(
352
+ eq(schema.crmSavedViews.id, input.viewId),
353
+ accessFilter(schema.crmSavedViews, schema.crmSavedViewShares),
354
+ ),
355
+ )
356
+ .limit(1)
357
+ : [];
358
+ if (input.viewId && !viewRow[0])
359
+ throw new Error("CRM saved view was not found.");
360
+ const view = viewRow[0] ? parseSavedViewConfig(viewRow[0]) : undefined;
361
+ if (input.kind && view?.kind && input.kind !== view.kind) {
362
+ throw new Error("CRM saved view kind cannot be overridden.");
363
+ }
364
+ if (input.query && view?.query && input.query !== view.query) {
365
+ throw new Error("CRM saved view query cannot be overridden.");
366
+ }
367
+ const kind = view?.kind ?? input.kind;
368
+ const query = view?.query ?? input.query;
369
+ const conditions = [
370
+ accessFilter(schema.crmRecords, schema.crmRecordShares),
371
+ accessFilter(schema.crmConnections, schema.crmConnectionShares),
372
+ eq(schema.crmRecords.tombstone, false),
373
+ ];
374
+
375
+ if (kind) conditions.push(eq(schema.crmRecords.kind, kind));
376
+ if (input.connectionId) {
377
+ conditions.push(eq(schema.crmRecords.connectionId, input.connectionId));
378
+ }
379
+ if (query) {
380
+ conditions.push(like(schema.crmRecords.displayName, `%${query}%`));
381
+ }
382
+ for (const [fieldName, value] of Object.entries(view?.fieldEquals ?? {})) {
383
+ conditions.push(fieldEqualsCondition(fieldName, value));
384
+ }
385
+
386
+ const rows = await db
387
+ .select({
388
+ id: schema.crmRecords.id,
389
+ displayName: schema.crmRecords.displayName,
390
+ kind: schema.crmRecords.kind,
391
+ primaryEmail: schema.crmRecords.primaryEmail,
392
+ domain: schema.crmRecords.domain,
393
+ stage: schema.crmRecords.stage,
394
+ ownerName: schema.crmRecords.ownerName,
395
+ nextContactAt: schema.crmRecords.nextContactAt,
396
+ remoteUpdatedAt: schema.crmRecords.remoteUpdatedAt,
397
+ updatedAt: schema.crmRecords.updatedAt,
398
+ connectionId: schema.crmRecords.connectionId,
399
+ objectType: schema.crmRecords.objectType,
400
+ provider: schema.crmRecords.provider,
401
+ accessScopeJson: schema.crmRecords.accessScopeJson,
402
+ workspaceConnectionId: schema.crmConnections.workspaceConnectionId,
403
+ })
404
+ .from(schema.crmRecords)
405
+ .innerJoin(
406
+ schema.crmConnections,
407
+ eq(schema.crmRecords.connectionId, schema.crmConnections.id),
408
+ )
409
+ .where(and(...conditions))
410
+ .orderBy(...orderForSavedView(view))
411
+ .limit(limit + 1)
412
+ .offset(offset);
413
+ const result = page(rows, limit);
414
+ const scopeResolver = options.resolveScope ?? defaultScopeResolver;
415
+ const scopeTargets = Array.from(
416
+ new Map(
417
+ result.rows.map((row) => [
418
+ `${row.connectionId}:${row.objectType}`,
419
+ {
420
+ connectionId: row.connectionId,
421
+ workspaceConnectionId: row.workspaceConnectionId,
422
+ provider: row.provider,
423
+ objectType: row.objectType,
424
+ },
425
+ ]),
426
+ ).values(),
427
+ ).slice(0, MAX_SCOPE_VALIDATIONS);
428
+ const currentScopes = new Map(
429
+ await Promise.all(
430
+ scopeTargets.map(
431
+ async (target) =>
432
+ [
433
+ `${target.connectionId}:${target.objectType}`,
434
+ await scopeResolver(target).catch(() => null),
435
+ ] as const,
436
+ ),
437
+ ),
438
+ );
439
+ const records = result.rows
440
+ .filter((row) => {
441
+ const current = currentScopes.get(
442
+ `${row.connectionId}:${row.objectType}`,
443
+ );
444
+ return Boolean(
445
+ current &&
446
+ scopesAreCompatible(parseCrmAccessScope(row.accessScopeJson), current),
447
+ );
448
+ })
449
+ .map((row) => toRecordSummary(row, view?.columns));
450
+
451
+ return {
452
+ records,
453
+ nextCursor: result.nextCursor ? String(offset + limit) : undefined,
454
+ complete: !result.nextCursor,
455
+ ...(view
456
+ ? {
457
+ appliedView: {
458
+ id: view.id,
459
+ name: view.name,
460
+ ...(view.kind ? { kind: view.kind } : {}),
461
+ ...(view.query ? { query: view.query } : {}),
462
+ ...(view.dataProgramId
463
+ ? { dataProgramId: view.dataProgramId }
464
+ : {}),
465
+ fieldEquals: view.fieldEquals,
466
+ columns: view.columns,
467
+ sort: view.sort,
468
+ },
469
+ }
470
+ : {}),
471
+ };
472
+ }
473
+
474
+ export type CrmRecordReadContext = {
475
+ id: string;
476
+ connectionId: string;
477
+ workspaceConnectionId: string | null;
478
+ provider: string;
479
+ objectType: string;
480
+ kind: CrmListKind;
481
+ remoteId: string;
482
+ accessScopeJson: string;
483
+ ownerEmail: string;
484
+ orgId: string | null;
485
+ visibility: "private" | "org" | "public";
486
+ fieldPolicies: Array<{
487
+ fieldName: string;
488
+ storagePolicy:
489
+ | "mirrored"
490
+ | "remote-only"
491
+ | "redacted"
492
+ | "derived-local"
493
+ | "local-authoritative";
494
+ readable: boolean;
495
+ sensitive: boolean;
496
+ }>;
497
+ };
498
+
499
+ export async function getCrmRecordReadContext(
500
+ recordId: string,
501
+ ): Promise<CrmRecordReadContext | null> {
502
+ const db = getDb();
503
+ const [record] = await db
504
+ .select({
505
+ id: schema.crmRecords.id,
506
+ connectionId: schema.crmRecords.connectionId,
507
+ workspaceConnectionId: schema.crmConnections.workspaceConnectionId,
508
+ provider: schema.crmRecords.provider,
509
+ objectType: schema.crmRecords.objectType,
510
+ kind: schema.crmRecords.kind,
511
+ remoteId: schema.crmRecords.remoteId,
512
+ accessScopeJson: schema.crmRecords.accessScopeJson,
513
+ ownerEmail: schema.crmRecords.ownerEmail,
514
+ orgId: schema.crmRecords.orgId,
515
+ visibility: schema.crmRecords.visibility,
516
+ })
517
+ .from(schema.crmRecords)
518
+ .innerJoin(
519
+ schema.crmConnections,
520
+ eq(schema.crmRecords.connectionId, schema.crmConnections.id),
521
+ )
522
+ .where(
523
+ and(
524
+ eq(schema.crmRecords.id, recordId),
525
+ eq(schema.crmRecords.tombstone, false),
526
+ accessFilter(schema.crmRecords, schema.crmRecordShares),
527
+ accessFilter(schema.crmConnections, schema.crmConnectionShares),
528
+ ),
529
+ )
530
+ .limit(1);
531
+ if (!record) return null;
532
+ const fieldPolicies = await db
533
+ .select({
534
+ fieldName: schema.crmFieldPolicies.fieldName,
535
+ storagePolicy: schema.crmFieldPolicies.storagePolicy,
536
+ readable: schema.crmFieldPolicies.readable,
537
+ sensitive: schema.crmFieldPolicies.sensitive,
538
+ })
539
+ .from(schema.crmFieldPolicies)
540
+ .where(
541
+ and(
542
+ eq(schema.crmFieldPolicies.connectionId, record.connectionId),
543
+ eq(schema.crmFieldPolicies.objectType, record.objectType),
544
+ accessFilter(schema.crmFieldPolicies, schema.crmFieldPolicyShares),
545
+ ),
546
+ );
547
+ return {
548
+ ...record,
549
+ kind: record.kind as CrmListKind,
550
+ visibility: record.visibility as "private" | "org" | "public",
551
+ fieldPolicies: fieldPolicies as CrmRecordReadContext["fieldPolicies"],
552
+ };
553
+ }
554
+
555
+ type ReadThroughRelationshipLocalRecord = {
556
+ id: string;
557
+ remoteId: string;
558
+ objectType: string;
559
+ displayName: string;
560
+ kind: string;
561
+ primaryEmail: string | null;
562
+ domain: string | null;
563
+ accessScopeJson: string;
564
+ };
565
+
566
+ type ReadThroughRelationshipData = {
567
+ relationships: CrmRelationship[];
568
+ localRecords: ReadThroughRelationshipLocalRecord[];
569
+ summaries: Array<
570
+ RelatedRecordSummary & { localId: string; remoteId: string }
571
+ >;
572
+ };
573
+
574
+ async function readThroughRelationshipData(input: {
575
+ context: CrmRecordReadContext;
576
+ relationships: CrmRelationship[];
577
+ currentScopes: Map<string, CrmAccessScope>;
578
+ }): Promise<ReadThroughRelationshipData> {
579
+ const relationships = input.relationships
580
+ .filter(
581
+ (relationship) =>
582
+ relationship.from.connectionId === input.context.connectionId &&
583
+ relationship.from.objectType === input.context.objectType &&
584
+ relationship.from.remoteId === input.context.remoteId &&
585
+ relationship.to.connectionId === input.context.connectionId,
586
+ )
587
+ .slice(0, 100);
588
+ if (!relationships.length) {
589
+ return { relationships: [], localRecords: [], summaries: [] };
590
+ }
591
+
592
+ const db = getDb();
593
+ const remoteIds = Array.from(
594
+ new Set(relationships.map((relationship) => relationship.to.remoteId)),
595
+ );
596
+ const objectTypes = Array.from(
597
+ new Set(relationships.map((relationship) => relationship.to.objectType)),
598
+ );
599
+ const candidates = await db
600
+ .select({
601
+ id: schema.crmRecords.id,
602
+ remoteId: schema.crmRecords.remoteId,
603
+ objectType: schema.crmRecords.objectType,
604
+ displayName: schema.crmRecords.displayName,
605
+ kind: schema.crmRecords.kind,
606
+ primaryEmail: schema.crmRecords.primaryEmail,
607
+ domain: schema.crmRecords.domain,
608
+ accessScopeJson: schema.crmRecords.accessScopeJson,
609
+ })
610
+ .from(schema.crmRecords)
611
+ .where(
612
+ and(
613
+ eq(schema.crmRecords.connectionId, input.context.connectionId),
614
+ eq(schema.crmRecords.tombstone, false),
615
+ inArray(schema.crmRecords.remoteId, remoteIds),
616
+ inArray(schema.crmRecords.objectType, objectTypes),
617
+ accessFilter(schema.crmRecords, schema.crmRecordShares),
618
+ ),
619
+ );
620
+ const localRecords = candidates.filter((record) => {
621
+ const current = input.currentScopes.get(record.objectType);
622
+ return Boolean(
623
+ current &&
624
+ scopesAreCompatible(parseCrmAccessScope(record.accessScopeJson), current),
625
+ );
626
+ });
627
+ const summaries = relatedSummaries(
628
+ input.context.remoteId,
629
+ relationships,
630
+ localRecords,
631
+ );
632
+ return { relationships, localRecords, summaries };
633
+ }
634
+
635
+ export async function getReadThroughRelationshipSummaries(input: {
636
+ context: CrmRecordReadContext;
637
+ relationships: CrmRelationship[];
638
+ currentScopes: Map<string, CrmAccessScope>;
639
+ }): Promise<RelatedRecordSummary[]> {
640
+ const { summaries } = await readThroughRelationshipData(input);
641
+ return summaries;
642
+ }
643
+
644
+ export async function persistReadThroughRelationships(input: {
645
+ context: CrmRecordReadContext;
646
+ relationships: CrmRelationship[];
647
+ currentScopes: Map<string, CrmAccessScope>;
648
+ }): Promise<RelatedRecordSummary[]> {
649
+ await assertAccess("crm-record", input.context.id, "editor");
650
+ const { relationships, localRecords, summaries } =
651
+ await readThroughRelationshipData(input);
652
+ if (!summaries.length) return [];
653
+
654
+ const db = getDb();
655
+ const relationshipByTarget = new Map(
656
+ relationships.map((relationship) => [
657
+ `${relationship.to.objectType}:${relationship.to.remoteId}:${relationship.relationshipType}`,
658
+ relationship,
659
+ ]),
660
+ );
661
+ const now = new Date().toISOString();
662
+ await db.transaction(async (tx) => {
663
+ for (const summary of summaries) {
664
+ const target = localRecords.find((record) => record.id === summary.id);
665
+ const relationship = target
666
+ ? relationshipByTarget.get(
667
+ `${target.objectType}:${summary.remoteId}:${summary.relationshipType}`,
668
+ )
669
+ : undefined;
670
+ if (!relationship) continue;
671
+ const [existing] = await tx
672
+ .select({ id: schema.crmRelationships.id })
673
+ .from(schema.crmRelationships)
674
+ .where(
675
+ and(
676
+ eq(
677
+ schema.crmRelationships.connectionId,
678
+ input.context.connectionId,
679
+ ),
680
+ eq(schema.crmRelationships.fromRecordId, input.context.id),
681
+ eq(schema.crmRelationships.toRecordId, summary.id),
682
+ eq(
683
+ schema.crmRelationships.relationshipType,
684
+ relationship.relationshipType,
685
+ ),
686
+ accessFilter(schema.crmRelationships, schema.crmRelationshipShares),
687
+ ),
688
+ )
689
+ .limit(1);
690
+ const values = {
691
+ label: relationship.label ?? null,
692
+ inverseLabel: relationship.inverseLabel ?? null,
693
+ sourceField: relationship.sourceField ?? null,
694
+ tombstone: false,
695
+ lastSyncedAt: now,
696
+ updatedAt: now,
697
+ };
698
+ if (existing) {
699
+ await tx
700
+ .update(schema.crmRelationships)
701
+ .set(values)
702
+ .where(
703
+ and(
704
+ eq(schema.crmRelationships.id, existing.id),
705
+ accessFilter(
706
+ schema.crmRelationships,
707
+ schema.crmRelationshipShares,
708
+ undefined,
709
+ "editor",
710
+ ),
711
+ ),
712
+ );
713
+ } else {
714
+ await tx.insert(schema.crmRelationships).values({
715
+ id: crypto.randomUUID(),
716
+ connectionId: input.context.connectionId,
717
+ fromRecordId: input.context.id,
718
+ toRecordId: summary.id,
719
+ relationshipType: relationship.relationshipType,
720
+ ...values,
721
+ ownerEmail: input.context.ownerEmail,
722
+ orgId: input.context.orgId,
723
+ visibility: input.context.visibility,
724
+ createdAt: now,
725
+ });
726
+ }
727
+ }
728
+ });
729
+ return summaries;
730
+ }
731
+
732
+ export async function getCrmRecord(
733
+ recordId: string,
734
+ readThrough?: {
735
+ displayName?: string;
736
+ fields?: Record<string, CrmValue>;
737
+ remoteRevision?: string;
738
+ remoteUpdatedAt?: string;
739
+ relatedRecords?: RelatedRecordSummary[];
740
+ accessScope?: CrmAccessScope;
741
+ },
742
+ ) {
743
+ const db = getDb();
744
+ const [record] = await db
745
+ .select({
746
+ id: schema.crmRecords.id,
747
+ provider: schema.crmRecords.provider,
748
+ displayName: schema.crmRecords.displayName,
749
+ kind: schema.crmRecords.kind,
750
+ primaryEmail: schema.crmRecords.primaryEmail,
751
+ domain: schema.crmRecords.domain,
752
+ stage: schema.crmRecords.stage,
753
+ ownerName: schema.crmRecords.ownerName,
754
+ desiredCadenceDays: schema.crmRecords.desiredCadenceDays,
755
+ nextContactAt: schema.crmRecords.nextContactAt,
756
+ remoteRevision: schema.crmRecords.remoteRevision,
757
+ remoteUpdatedAt: schema.crmRecords.remoteUpdatedAt,
758
+ updatedAt: schema.crmRecords.updatedAt,
759
+ accessScopeJson: schema.crmRecords.accessScopeJson,
760
+ })
761
+ .from(schema.crmRecords)
762
+ .where(
763
+ and(
764
+ accessFilter(schema.crmRecords, schema.crmRecordShares),
765
+ eq(schema.crmRecords.id, recordId),
766
+ eq(schema.crmRecords.tombstone, false),
767
+ ),
768
+ )
769
+ .limit(1);
770
+
771
+ if (!record) return null;
772
+
773
+ const [fieldRows, activityRows, evidenceRows, taskRows] = await Promise.all([
774
+ db
775
+ .select({
776
+ fieldName: schema.crmRecordFields.fieldName,
777
+ valueType: schema.crmRecordFields.valueType,
778
+ stringValue: schema.crmRecordFields.stringValue,
779
+ numberValue: schema.crmRecordFields.numberValue,
780
+ booleanValue: schema.crmRecordFields.booleanValue,
781
+ jsonValue: schema.crmRecordFields.jsonValue,
782
+ accessScopeJson: schema.crmRecordFields.accessScopeJson,
783
+ })
784
+ .from(schema.crmRecordFields)
785
+ .where(
786
+ and(
787
+ accessFilter(schema.crmRecordFields, schema.crmRecordFieldShares),
788
+ eq(schema.crmRecordFields.recordId, recordId),
789
+ ),
790
+ )
791
+ .orderBy(desc(schema.crmRecordFields.updatedAt))
792
+ .limit(MAX_RECORD_LIMIT),
793
+ db
794
+ .select({
795
+ id: schema.crmInteractions.id,
796
+ title: schema.crmInteractions.title,
797
+ summary: schema.crmInteractions.summary,
798
+ occurredAt: schema.crmInteractions.occurredAt,
799
+ sourceApp: schema.crmInteractions.sourceApp,
800
+ })
801
+ .from(schema.crmInteractions)
802
+ .where(
803
+ and(
804
+ accessFilter(schema.crmInteractions, schema.crmInteractionShares),
805
+ eq(schema.crmInteractions.recordId, recordId),
806
+ ),
807
+ )
808
+ .orderBy(desc(schema.crmInteractions.occurredAt))
809
+ .limit(RECORD_DETAIL_LIMIT),
810
+ db
811
+ .select({
812
+ id: schema.crmCallEvidence.id,
813
+ artifactType: schema.crmCallEvidence.artifactType,
814
+ quote: schema.crmCallEvidence.quote,
815
+ sourceUrl: schema.crmCallEvidence.sourceUrl,
816
+ capturedAt: schema.crmCallEvidence.capturedAt,
817
+ summary: schema.crmCallEvidence.summary,
818
+ })
819
+ .from(schema.crmCallEvidence)
820
+ .where(
821
+ and(
822
+ accessFilter(schema.crmCallEvidence, schema.crmCallEvidenceShares),
823
+ eq(schema.crmCallEvidence.recordId, recordId),
824
+ ),
825
+ )
826
+ .orderBy(desc(schema.crmCallEvidence.capturedAt))
827
+ .limit(RECORD_DETAIL_LIMIT),
828
+ db
829
+ .select({
830
+ id: schema.crmTasks.id,
831
+ title: schema.crmTasks.title,
832
+ status: schema.crmTasks.status,
833
+ dueAt: schema.crmTasks.dueAt,
834
+ recordId: schema.crmTasks.recordId,
835
+ })
836
+ .from(schema.crmTasks)
837
+ .where(
838
+ and(
839
+ accessFilter(schema.crmTasks, schema.crmTaskShares),
840
+ eq(schema.crmTasks.recordId, recordId),
841
+ ),
842
+ )
843
+ .orderBy(desc(schema.crmTasks.updatedAt))
844
+ .limit(RECORD_DETAIL_LIMIT),
845
+ ]);
846
+
847
+ return {
848
+ ...toRecordSummary(record),
849
+ provider: record.provider,
850
+ ...(readThrough?.displayName
851
+ ? { displayName: readThrough.displayName }
852
+ : {}),
853
+ ...(readThrough?.remoteUpdatedAt
854
+ ? { updatedAt: readThrough.remoteUpdatedAt }
855
+ : {}),
856
+ ...(readThrough?.remoteRevision
857
+ ? { remoteRevision: readThrough.remoteRevision }
858
+ : record.remoteRevision
859
+ ? { remoteRevision: record.remoteRevision }
860
+ : {}),
861
+ cadence: record.desiredCadenceDays
862
+ ? `Every ${record.desiredCadenceDays} days`
863
+ : undefined,
864
+ fields: {
865
+ ...Object.fromEntries(
866
+ fieldRows.flatMap((field) => {
867
+ const scope =
868
+ readThrough?.accessScope ??
869
+ parseCrmAccessScope(record.accessScopeJson);
870
+ if (
871
+ !scope ||
872
+ !scopesAreCompatible(
873
+ parseCrmAccessScope(field.accessScopeJson),
874
+ scope,
875
+ )
876
+ ) {
877
+ return [];
878
+ }
879
+ const value = parseStoredValue(field);
880
+ return value === null ? [] : [[field.fieldName, value]];
881
+ }),
882
+ ),
883
+ ...(readThrough?.fields ?? {}),
884
+ },
885
+ activity: activityRows.map((activity) => ({
886
+ ...activity,
887
+ actor: activity.sourceApp ?? undefined,
888
+ })),
889
+ evidence: evidenceRows.map((evidence) => ({
890
+ id: evidence.id,
891
+ label: evidence.summary || evidence.artifactType,
892
+ quote: evidence.quote || undefined,
893
+ url: evidence.sourceUrl,
894
+ observedAt: evidence.capturedAt,
895
+ })),
896
+ tasks: taskRows,
897
+ relatedRecords: readThrough?.relatedRecords ?? [],
898
+ };
899
+ }
900
+
901
+ export async function listCrmTasks(input: {
902
+ recordId?: string;
903
+ status?: "open" | "done" | "cancelled";
904
+ limit: number;
905
+ cursor?: string;
906
+ }) {
907
+ const db = getDb();
908
+ const offset = decodeCursor(input.cursor);
909
+ const limit = Math.min(input.limit, MAX_RECORD_LIMIT);
910
+ const conditions = [accessFilter(schema.crmTasks, schema.crmTaskShares)];
911
+ if (input.recordId)
912
+ conditions.push(eq(schema.crmTasks.recordId, input.recordId));
913
+ if (input.status) conditions.push(eq(schema.crmTasks.status, input.status));
914
+
915
+ const rows = await db
916
+ .select({
917
+ id: schema.crmTasks.id,
918
+ title: schema.crmTasks.title,
919
+ status: schema.crmTasks.status,
920
+ dueAt: schema.crmTasks.dueAt,
921
+ recordId: schema.crmTasks.recordId,
922
+ assignedTo: schema.crmTasks.assignedTo,
923
+ authority: schema.crmTasks.authority,
924
+ updatedAt: schema.crmTasks.updatedAt,
925
+ })
926
+ .from(schema.crmTasks)
927
+ .where(and(...conditions))
928
+ .orderBy(desc(schema.crmTasks.dueAt), desc(schema.crmTasks.id))
929
+ .limit(limit + 1)
930
+ .offset(offset);
931
+ const result = page(rows, limit);
932
+
933
+ return {
934
+ tasks: result.rows,
935
+ nextCursor: result.nextCursor ? String(offset + limit) : undefined,
936
+ complete: !result.nextCursor,
937
+ };
938
+ }
939
+
940
+ export async function listCrmSavedViews(input: { limit: number }) {
941
+ const rows = await getDb()
942
+ .select({
943
+ id: schema.crmSavedViews.id,
944
+ name: schema.crmSavedViews.name,
945
+ description: schema.crmSavedViews.description,
946
+ kind: schema.crmSavedViews.kind,
947
+ filtersJson: schema.crmSavedViews.filtersJson,
948
+ dataProgramId: schema.crmSavedViews.dataProgramId,
949
+ pinned: schema.crmSavedViews.pinned,
950
+ updatedAt: schema.crmSavedViews.updatedAt,
951
+ })
952
+ .from(schema.crmSavedViews)
953
+ .where(accessFilter(schema.crmSavedViews, schema.crmSavedViewShares))
954
+ .orderBy(
955
+ desc(schema.crmSavedViews.pinned),
956
+ desc(schema.crmSavedViews.updatedAt),
957
+ )
958
+ .limit(Math.min(input.limit, MAX_RECORD_LIMIT));
959
+
960
+ return {
961
+ views: rows.map((view) => ({
962
+ id: view.id,
963
+ name: view.name,
964
+ description: view.description || undefined,
965
+ kind: view.kind ?? undefined,
966
+ query: view.filtersJson === "{}" ? undefined : view.filtersJson,
967
+ dataProgramId: view.dataProgramId ?? undefined,
968
+ pinned: view.pinned,
969
+ updatedAt: view.updatedAt,
970
+ })),
971
+ };
972
+ }
973
+
974
+ export async function listCrmSignals(input: {
975
+ recordId: string;
976
+ limit: number;
977
+ }) {
978
+ return getDb()
979
+ .select({
980
+ id: schema.crmSignals.id,
981
+ label: schema.crmSignals.label,
982
+ kind: schema.crmSignals.kind,
983
+ confidence: schema.crmSignals.confidence,
984
+ reviewStatus: schema.crmSignals.reviewStatus,
985
+ evidenceId: schema.crmSignals.evidenceId,
986
+ startSeconds: schema.crmSignals.startSeconds,
987
+ createdAt: schema.crmSignals.createdAt,
988
+ })
989
+ .from(schema.crmSignals)
990
+ .where(
991
+ and(
992
+ eq(schema.crmSignals.recordId, input.recordId),
993
+ accessFilter(schema.crmSignals, schema.crmSignalShares),
994
+ ),
995
+ )
996
+ .orderBy(desc(schema.crmSignals.createdAt))
997
+ .limit(Math.min(Math.max(input.limit, 1), 100));
998
+ }
999
+
1000
+ export function safeProposalValues(value: string): Record<string, Primitive> {
1001
+ const parsed = parseJsonObject(value);
1002
+ const fields = parsed?.fields;
1003
+ const source =
1004
+ fields && typeof fields === "object" && !Array.isArray(fields)
1005
+ ? (fields as Record<string, unknown>)
1006
+ : (parsed ?? {});
1007
+ return Object.fromEntries(
1008
+ Object.entries(source)
1009
+ .filter(
1010
+ ([fieldName, fieldValue]) =>
1011
+ fieldName.length <= 120 &&
1012
+ isSafeCrmMutationFieldName(fieldName) &&
1013
+ isBoundedCrmValue(fieldValue) &&
1014
+ parsePrimitive(fieldValue),
1015
+ )
1016
+ .slice(0, 20),
1017
+ ) as Record<string, Primitive>;
1018
+ }
1019
+
1020
+ export async function listCrmProposals(input: {
1021
+ recordId?: string;
1022
+ status?:
1023
+ | "pending"
1024
+ | "approved"
1025
+ | "applied"
1026
+ | "rejected"
1027
+ | "conflict"
1028
+ | "failed";
1029
+ limit: number;
1030
+ cursor?: string;
1031
+ }) {
1032
+ const db = getDb();
1033
+ const offset = decodeCursor(input.cursor);
1034
+ const limit = Math.min(input.limit, MAX_RECORD_LIMIT);
1035
+ const conditions = [
1036
+ accessFilter(schema.crmMutations, schema.crmMutationShares),
1037
+ ];
1038
+ if (input.recordId) {
1039
+ conditions.push(eq(schema.crmMutations.recordId, input.recordId));
1040
+ }
1041
+ if (input.status)
1042
+ conditions.push(eq(schema.crmMutations.status, input.status));
1043
+
1044
+ const rows = await db
1045
+ .select({
1046
+ id: schema.crmMutations.id,
1047
+ recordId: schema.crmMutations.recordId,
1048
+ operation: schema.crmMutations.operation,
1049
+ initiatedBy: schema.crmMutations.initiatedBy,
1050
+ target: schema.crmMutations.target,
1051
+ policyDecision: schema.crmMutations.policyDecision,
1052
+ risk: schema.crmMutations.risk,
1053
+ status: schema.crmMutations.status,
1054
+ expectedRemoteRevision: schema.crmMutations.expectedRemoteRevision,
1055
+ patchJson: schema.crmMutations.patchJson,
1056
+ beforeJson: schema.crmMutations.beforeJson,
1057
+ afterJson: schema.crmMutations.afterJson,
1058
+ createdAt: schema.crmMutations.createdAt,
1059
+ appliedAt: schema.crmMutations.appliedAt,
1060
+ })
1061
+ .from(schema.crmMutations)
1062
+ .where(and(...conditions))
1063
+ .orderBy(desc(schema.crmMutations.createdAt), desc(schema.crmMutations.id))
1064
+ .limit(limit + 1)
1065
+ .offset(offset);
1066
+ const result = page(rows, limit);
1067
+ const recordIds = result.rows
1068
+ .map((row) => row.recordId)
1069
+ .filter((recordId): recordId is string => Boolean(recordId));
1070
+ const records = recordIds.length
1071
+ ? await db
1072
+ .select({
1073
+ id: schema.crmRecords.id,
1074
+ displayName: schema.crmRecords.displayName,
1075
+ provider: schema.crmRecords.provider,
1076
+ })
1077
+ .from(schema.crmRecords)
1078
+ .where(
1079
+ and(
1080
+ inArray(schema.crmRecords.id, recordIds),
1081
+ accessFilter(schema.crmRecords, schema.crmRecordShares),
1082
+ ),
1083
+ )
1084
+ : [];
1085
+ const displayNameByRecordId = new Map(
1086
+ records.map((record) => [record.id, record.displayName]),
1087
+ );
1088
+ const providerByRecordId = new Map(
1089
+ records.map((record) => [record.id, record.provider]),
1090
+ );
1091
+
1092
+ return {
1093
+ proposals: result.rows.map((proposal) => {
1094
+ const patch = safeProposalValues(proposal.patchJson);
1095
+ const before = safeProposalValues(proposal.beforeJson);
1096
+ const after = safeProposalValues(proposal.afterJson);
1097
+ const fieldNames = Array.from(
1098
+ new Set([
1099
+ ...Object.keys(before),
1100
+ ...Object.keys(after),
1101
+ ...Object.keys(patch),
1102
+ ]),
1103
+ ).slice(0, 20);
1104
+ return {
1105
+ id: proposal.id,
1106
+ recordId: proposal.recordId,
1107
+ ...(proposal.recordId && displayNameByRecordId.get(proposal.recordId)
1108
+ ? { recordName: displayNameByRecordId.get(proposal.recordId) }
1109
+ : {}),
1110
+ ...(proposal.recordId && providerByRecordId.get(proposal.recordId)
1111
+ ? { provider: providerByRecordId.get(proposal.recordId) }
1112
+ : {}),
1113
+ operation: proposal.operation,
1114
+ initiatedBy: proposal.initiatedBy,
1115
+ target: proposal.target,
1116
+ policyDecision: proposal.policyDecision,
1117
+ risk: proposal.risk,
1118
+ status: proposal.status,
1119
+ expectedRemoteRevision: proposal.expectedRemoteRevision,
1120
+ createdAt: proposal.createdAt,
1121
+ appliedAt: proposal.appliedAt,
1122
+ fields: fieldNames.map((name) => ({
1123
+ name,
1124
+ ...(Object.prototype.hasOwnProperty.call(before, name)
1125
+ ? { before: before[name] }
1126
+ : {}),
1127
+ ...(Object.prototype.hasOwnProperty.call(
1128
+ Object.keys(after).length ? after : patch,
1129
+ name,
1130
+ )
1131
+ ? { after: (Object.keys(after).length ? after : patch)[name] }
1132
+ : {}),
1133
+ })),
1134
+ };
1135
+ }),
1136
+ nextCursor: result.nextCursor ? String(offset + limit) : undefined,
1137
+ complete: !result.nextCursor,
1138
+ };
1139
+ }
1140
+
1141
+ export async function getCrmOverview() {
1142
+ const [taskResult, recordResult, proposalResult] = await Promise.all([
1143
+ listCrmTasks({ status: "open", limit: 8 }),
1144
+ listCrmRecords({ limit: 5 }),
1145
+ listCrmProposals({ status: "pending", limit: 1 }),
1146
+ ]);
1147
+
1148
+ return {
1149
+ tasks: taskResult.tasks,
1150
+ records: recordResult.records,
1151
+ focus: [
1152
+ {
1153
+ label: "Open follow-up",
1154
+ value: String(taskResult.tasks.length),
1155
+ detail: taskResult.complete
1156
+ ? "Tasks currently due for attention."
1157
+ : "Showing the first 8 tasks due for attention.",
1158
+ },
1159
+ ...(proposalResult.proposals.length
1160
+ ? [
1161
+ {
1162
+ label: "Pending proposal",
1163
+ value: "1+",
1164
+ detail: "A CRM write is waiting for review.",
1165
+ },
1166
+ ]
1167
+ : []),
1168
+ ],
1169
+ };
1170
+ }