@agent-native/core 0.123.0 → 0.123.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (263) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/core/CHANGELOG.md +16 -0
  3. package/corpus/core/package.json +1 -1
  4. package/corpus/core/src/cli/templates-meta.ts +12 -0
  5. package/corpus/templates/crm/.agents/skills/agent-native-docs/SKILL.md +115 -0
  6. package/corpus/templates/crm/.agents/skills/agent-native-toolkit/SKILL.md +177 -0
  7. package/corpus/templates/crm/.agents/skills/crm/SKILL.md +205 -0
  8. package/corpus/templates/crm/.agents/skills/customizing-agent-native/SKILL.md +220 -0
  9. package/corpus/templates/crm/.agents/skills/feature-flags/SKILL.md +169 -0
  10. package/corpus/templates/crm/.agents/skills/sharing/SKILL.md +228 -0
  11. package/corpus/templates/crm/.agents/skills/storing-data/SKILL.md +178 -0
  12. package/corpus/templates/crm/.agents/skills/upgrade-agent-native/SKILL.md +122 -0
  13. package/corpus/templates/crm/.env.example +5 -0
  14. package/corpus/templates/crm/AGENTS.md +82 -0
  15. package/corpus/templates/crm/CHANGELOG.md +25 -0
  16. package/corpus/templates/crm/DEVELOPING.md +66 -0
  17. package/corpus/templates/crm/README.md +122 -0
  18. package/corpus/templates/crm/_gitignore +12 -0
  19. package/corpus/templates/crm/actions/_crm-action-utils.ts +98 -0
  20. package/corpus/templates/crm/actions/_crm-attribute-utils.ts +219 -0
  21. package/corpus/templates/crm/actions/_crm-dashboard.ts +69 -0
  22. package/corpus/templates/crm/actions/_crm-data-program-actions.ts +15 -0
  23. package/corpus/templates/crm/actions/_crm-list-utils.ts +775 -0
  24. package/corpus/templates/crm/actions/_crm-signal-utils.ts +79 -0
  25. package/corpus/templates/crm/actions/add-crm-record-to-list.ts +164 -0
  26. package/corpus/templates/crm/actions/apply-crm-proposals.ts +291 -0
  27. package/corpus/templates/crm/actions/archive-crm-attribute.ts +69 -0
  28. package/corpus/templates/crm/actions/attach-call-evidence.ts +170 -0
  29. package/corpus/templates/crm/actions/configure-crm-connection.ts +126 -0
  30. package/corpus/templates/crm/actions/configure-native-crm.ts +44 -0
  31. package/corpus/templates/crm/actions/create-crm-attribute.ts +350 -0
  32. package/corpus/templates/crm/actions/create-crm-list.ts +194 -0
  33. package/corpus/templates/crm/actions/create-crm-record.ts +148 -0
  34. package/corpus/templates/crm/actions/create-crm-signal-tracker.ts +58 -0
  35. package/corpus/templates/crm/actions/delete-crm-saved-view.ts +68 -0
  36. package/corpus/templates/crm/actions/delete-staged-dataset.ts +14 -0
  37. package/corpus/templates/crm/actions/estimate-crm-enrichment.ts +229 -0
  38. package/corpus/templates/crm/actions/find-crm-duplicates.ts +87 -0
  39. package/corpus/templates/crm/actions/get-crm-automation-recipe.ts +36 -0
  40. package/corpus/templates/crm/actions/get-crm-dashboard-panel.ts +35 -0
  41. package/corpus/templates/crm/actions/get-crm-dashboard.ts +14 -0
  42. package/corpus/templates/crm/actions/get-crm-overview.ts +13 -0
  43. package/corpus/templates/crm/actions/get-crm-pipeline-data.ts +61 -0
  44. package/corpus/templates/crm/actions/get-crm-record-page.ts +287 -0
  45. package/corpus/templates/crm/actions/get-crm-record.ts +75 -0
  46. package/corpus/templates/crm/actions/get-crm-workspace.ts +364 -0
  47. package/corpus/templates/crm/actions/install-crm-pipeline-dashboard.ts +81 -0
  48. package/corpus/templates/crm/actions/list-crm-attributes.ts +71 -0
  49. package/corpus/templates/crm/actions/list-crm-connections.ts +68 -0
  50. package/corpus/templates/crm/actions/list-crm-dashboard-revisions.ts +15 -0
  51. package/corpus/templates/crm/actions/list-crm-dashboards.ts +15 -0
  52. package/corpus/templates/crm/actions/list-crm-enrichment-slots.ts +28 -0
  53. package/corpus/templates/crm/actions/list-crm-list-entries.ts +192 -0
  54. package/corpus/templates/crm/actions/list-crm-lists.ts +80 -0
  55. package/corpus/templates/crm/actions/list-crm-proposals.ts +55 -0
  56. package/corpus/templates/crm/actions/list-crm-record-field-history.ts +201 -0
  57. package/corpus/templates/crm/actions/list-crm-record-values.ts +200 -0
  58. package/corpus/templates/crm/actions/list-crm-records.ts +103 -0
  59. package/corpus/templates/crm/actions/list-crm-saved-views.ts +66 -0
  60. package/corpus/templates/crm/actions/list-crm-signal-hits.ts +43 -0
  61. package/corpus/templates/crm/actions/list-crm-signal-trackers.ts +32 -0
  62. package/corpus/templates/crm/actions/list-crm-tasks.ts +22 -0
  63. package/corpus/templates/crm/actions/list-staged-datasets.ts +14 -0
  64. package/corpus/templates/crm/actions/list-workspace-connections.ts +47 -0
  65. package/corpus/templates/crm/actions/manage-crm-attribute-option.ts +272 -0
  66. package/corpus/templates/crm/actions/manage-crm-signal-tracker.ts +87 -0
  67. package/corpus/templates/crm/actions/manage-crm-task.ts +132 -0
  68. package/corpus/templates/crm/actions/merge-crm-records.ts +691 -0
  69. package/corpus/templates/crm/actions/navigate.ts +54 -0
  70. package/corpus/templates/crm/actions/provider-api-catalog.ts +22 -0
  71. package/corpus/templates/crm/actions/provider-api-docs.ts +54 -0
  72. package/corpus/templates/crm/actions/provider-api-request.ts +92 -0
  73. package/corpus/templates/crm/actions/query-staged-dataset.ts +44 -0
  74. package/corpus/templates/crm/actions/record-crm-call-insight.ts +160 -0
  75. package/corpus/templates/crm/actions/record-crm-smart-signal.ts +150 -0
  76. package/corpus/templates/crm/actions/remove-crm-list-entry.ts +79 -0
  77. package/corpus/templates/crm/actions/restore-crm-dashboard-revision.ts +23 -0
  78. package/corpus/templates/crm/actions/review-crm-signal.ts +31 -0
  79. package/corpus/templates/crm/actions/run-crm-attribute-fill.ts +443 -0
  80. package/corpus/templates/crm/actions/run-crm-enrichment.ts +647 -0
  81. package/corpus/templates/crm/actions/run-crm-saved-view-program.ts +103 -0
  82. package/corpus/templates/crm/actions/run-crm-signal-trackers.ts +265 -0
  83. package/corpus/templates/crm/actions/run.ts +7 -0
  84. package/corpus/templates/crm/actions/save-crm-dashboard.ts +51 -0
  85. package/corpus/templates/crm/actions/save-crm-saved-view.ts +296 -0
  86. package/corpus/templates/crm/actions/sync-crm.ts +146 -0
  87. package/corpus/templates/crm/actions/update-crm-attribute.ts +111 -0
  88. package/corpus/templates/crm/actions/update-crm-list-entry.ts +102 -0
  89. package/corpus/templates/crm/actions/update-crm-list.ts +97 -0
  90. package/corpus/templates/crm/actions/update-crm-record.ts +543 -0
  91. package/corpus/templates/crm/actions/view-screen.ts +324 -0
  92. package/corpus/templates/crm/agent-native.app-skill.json +69 -0
  93. package/corpus/templates/crm/app/components/crm/CreateCrmRecordDialog.tsx +158 -0
  94. package/corpus/templates/crm/app/components/crm/CrmDashboardPanel.tsx +78 -0
  95. package/corpus/templates/crm/app/components/crm/CrmSignalsPanel.tsx +220 -0
  96. package/corpus/templates/crm/app/components/crm/IntelligenceSettings.tsx +414 -0
  97. package/corpus/templates/crm/app/components/crm/RecordActions.tsx +648 -0
  98. package/corpus/templates/crm/app/components/crm/RecordGrid.tsx +488 -0
  99. package/corpus/templates/crm/app/components/crm/RecordWorkspace.tsx +232 -0
  100. package/corpus/templates/crm/app/components/crm/SavedViewDataProgram.tsx +93 -0
  101. package/corpus/templates/crm/app/components/crm/Surface.tsx +97 -0
  102. package/corpus/templates/crm/app/components/crm/WorkOverview.tsx +180 -0
  103. package/corpus/templates/crm/app/components/crm/board/CrmBoard.tsx +988 -0
  104. package/corpus/templates/crm/app/components/crm/board/board-model.ts +399 -0
  105. package/corpus/templates/crm/app/components/crm/board/entry-filter.ts +150 -0
  106. package/corpus/templates/crm/app/components/crm/board/view-draft.ts +227 -0
  107. package/corpus/templates/crm/app/components/crm/grid/CrmGrid.tsx +1033 -0
  108. package/corpus/templates/crm/app/components/crm/grid/GridCell.tsx +620 -0
  109. package/corpus/templates/crm/app/components/crm/grid/clipboard.ts +84 -0
  110. package/corpus/templates/crm/app/components/crm/grid/model.ts +304 -0
  111. package/corpus/templates/crm/app/components/crm/grid/navigation.ts +220 -0
  112. package/corpus/templates/crm/app/components/crm/grid/query.ts +214 -0
  113. package/corpus/templates/crm/app/components/crm/record/AttributePanel.tsx +192 -0
  114. package/corpus/templates/crm/app/components/crm/record/FieldHistory.tsx +179 -0
  115. package/corpus/templates/crm/app/components/crm/record/RecordHeader.tsx +103 -0
  116. package/corpus/templates/crm/app/components/crm/record/RecordLists.tsx +235 -0
  117. package/corpus/templates/crm/app/components/crm/record/RecordTabs.tsx +206 -0
  118. package/corpus/templates/crm/app/components/crm/record/field-editors.tsx +245 -0
  119. package/corpus/templates/crm/app/components/crm/record/record-data.ts +603 -0
  120. package/corpus/templates/crm/app/components/crm/settings/AdvancedSettings.tsx +49 -0
  121. package/corpus/templates/crm/app/components/crm/settings/AttributeOptionsEditor.tsx +441 -0
  122. package/corpus/templates/crm/app/components/crm/settings/ConnectionSettings.tsx +169 -0
  123. package/corpus/templates/crm/app/components/crm/settings/FieldsSettings.tsx +935 -0
  124. package/corpus/templates/crm/app/components/crm/settings/ListsSettings.tsx +339 -0
  125. package/corpus/templates/crm/app/components/crm/settings/settings-admin.ts +377 -0
  126. package/corpus/templates/crm/app/components/crm/shared/AttributeValueParts.tsx +69 -0
  127. package/corpus/templates/crm/app/components/crm/shared/attribute-value.ts +591 -0
  128. package/corpus/templates/crm/app/components/layout/CrmCommandMenu.tsx +556 -0
  129. package/corpus/templates/crm/app/components/layout/CrmLayout.tsx +130 -0
  130. package/corpus/templates/crm/app/components/layout/CrmSidebar.tsx +407 -0
  131. package/corpus/templates/crm/app/components/layout/command-palette-search.ts +126 -0
  132. package/corpus/templates/crm/app/components/layout/crm-ui-intents.ts +41 -0
  133. package/corpus/templates/crm/app/components/layout/sidebar-collapse.ts +40 -0
  134. package/corpus/templates/crm/app/components/layout/sidebar-lists.ts +109 -0
  135. package/corpus/templates/crm/app/components/ui/alert-dialog.tsx +1 -0
  136. package/corpus/templates/crm/app/components/ui/badge.tsx +1 -0
  137. package/corpus/templates/crm/app/components/ui/button.tsx +1 -0
  138. package/corpus/templates/crm/app/components/ui/card.tsx +1 -0
  139. package/corpus/templates/crm/app/components/ui/checkbox.tsx +1 -0
  140. package/corpus/templates/crm/app/components/ui/dialog.tsx +1 -0
  141. package/corpus/templates/crm/app/components/ui/dropdown-menu.tsx +1 -0
  142. package/corpus/templates/crm/app/components/ui/hover-card.tsx +1 -0
  143. package/corpus/templates/crm/app/components/ui/input.tsx +1 -0
  144. package/corpus/templates/crm/app/components/ui/label.tsx +1 -0
  145. package/corpus/templates/crm/app/components/ui/popover.tsx +1 -0
  146. package/corpus/templates/crm/app/components/ui/select.tsx +1 -0
  147. package/corpus/templates/crm/app/components/ui/sheet.tsx +1 -0
  148. package/corpus/templates/crm/app/components/ui/skeleton.tsx +1 -0
  149. package/corpus/templates/crm/app/components/ui/switch.tsx +1 -0
  150. package/corpus/templates/crm/app/components/ui/table.tsx +1 -0
  151. package/corpus/templates/crm/app/components/ui/tabs.tsx +1 -0
  152. package/corpus/templates/crm/app/components/ui/textarea.tsx +1 -0
  153. package/corpus/templates/crm/app/components/ui/toolkit-provider.tsx +17 -0
  154. package/corpus/templates/crm/app/components/ui/tooltip.tsx +1 -0
  155. package/corpus/templates/crm/app/design-system.ts +3 -0
  156. package/corpus/templates/crm/app/entry.client.tsx +19 -0
  157. package/corpus/templates/crm/app/entry.server.tsx +10 -0
  158. package/corpus/templates/crm/app/global.css +167 -0
  159. package/corpus/templates/crm/app/hooks/use-keyboard-shortcuts.ts +206 -0
  160. package/corpus/templates/crm/app/hooks/use-navigation-state.ts +48 -0
  161. package/corpus/templates/crm/app/i18n/en-US.ts +684 -0
  162. package/corpus/templates/crm/app/i18n/index.ts +34 -0
  163. package/corpus/templates/crm/app/lib/dashboard.ts +13 -0
  164. package/corpus/templates/crm/app/lib/navigation.ts +17 -0
  165. package/corpus/templates/crm/app/lib/tab-id.ts +1 -0
  166. package/corpus/templates/crm/app/lib/types.ts +172 -0
  167. package/corpus/templates/crm/app/lib/utils.ts +1 -0
  168. package/corpus/templates/crm/app/root.tsx +93 -0
  169. package/corpus/templates/crm/app/routes/_index.tsx +26 -0
  170. package/corpus/templates/crm/app/routes/accounts.tsx +41 -0
  171. package/corpus/templates/crm/app/routes/agent.tsx +5 -0
  172. package/corpus/templates/crm/app/routes/ask.tsx +53 -0
  173. package/corpus/templates/crm/app/routes/dashboard.tsx +118 -0
  174. package/corpus/templates/crm/app/routes/lists.tsx +228 -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 +388 -0
  178. package/corpus/templates/crm/app/routes/records.$recordId.tsx +53 -0
  179. package/corpus/templates/crm/app/routes/records.tsx +64 -0
  180. package/corpus/templates/crm/app/routes/settings.tsx +118 -0
  181. package/corpus/templates/crm/app/routes/setup.tsx +308 -0
  182. package/corpus/templates/crm/app/routes/tasks.tsx +252 -0
  183. package/corpus/templates/crm/app/routes/views.tsx +683 -0
  184. package/corpus/templates/crm/app/routes.ts +19 -0
  185. package/corpus/templates/crm/app/vite-env.d.ts +6 -0
  186. package/corpus/templates/crm/changelog/2026-07-26-any-list-or-view-can-be-shown-as-a-board-grouped-by-stage-so.md +6 -0
  187. package/corpus/templates/crm/changelog/2026-07-26-crm-now-finds-likely-duplicate-records-and-tells-you-exactly.md +6 -0
  188. package/corpus/templates/crm/changelog/2026-07-26-enrichment-now-shows-what-a-run-will-cost-before-it-starts-g.md +6 -0
  189. package/corpus/templates/crm/changelog/2026-07-26-lists-turn-any-set-of-records-into-a-pipeline-with-its-own-s.md +6 -0
  190. package/corpus/templates/crm/changelog/2026-07-26-moving-a-record-or-list-entry-into-a-stage-that-was-retired-.md +6 -0
  191. package/corpus/templates/crm/changelog/2026-07-26-opening-a-record-now-shows-one-page-with-its-fields-the-list.md +6 -0
  192. package/corpus/templates/crm/changelog/2026-07-26-press-cmd-k-to-jump-to-any-record-list-view-or-setting-and-u.md +6 -0
  193. package/corpus/templates/crm/changelog/2026-07-26-provider-record-edits-now-show-an-exact-before-after-diff-an.md +6 -0
  194. package/corpus/templates/crm/changelog/2026-07-26-record-page-attribute-labels-now-read-in-full-instead-of-bei.md +6 -0
  195. package/corpus/templates/crm/changelog/2026-07-26-records-now-use-typed-fields-currency-dates-ratings-status-s.md +6 -0
  196. package/corpus/templates/crm/changelog/2026-07-26-retyping-a-value-on-the-record-page-now-replaces-it-instead-.md +6 -0
  197. package/corpus/templates/crm/changelog/2026-07-26-settings-now-has-fields-and-lists-tabs-for-adding-fields-edi.md +6 -0
  198. package/corpus/templates/crm/changelog/2026-07-26-the-proposal-review-now-shows-the-field-that-actually-change.md +6 -0
  199. package/corpus/templates/crm/changelog/2026-07-26-the-record-grid-is-now-a-real-spreadsheet-filter-sort-and-pa.md +6 -0
  200. package/corpus/templates/crm/components.json +20 -0
  201. package/corpus/templates/crm/docs/architecture/crm-contract.md +214 -0
  202. package/corpus/templates/crm/docs/features/README.md +55 -0
  203. package/corpus/templates/crm/docs/features/f1-records-and-fields.mdx +52 -0
  204. package/corpus/templates/crm/docs/features/f2-lists-pipelines-and-views.mdx +48 -0
  205. package/corpus/templates/crm/docs/features/f3-work-and-record-quality.mdx +40 -0
  206. package/corpus/templates/crm/docs/features/f4-evidence-and-chat.mdx +47 -0
  207. package/corpus/templates/crm/learnings.defaults.md +5 -0
  208. package/corpus/templates/crm/netlify.toml +11 -0
  209. package/corpus/templates/crm/package.json +61 -0
  210. package/corpus/templates/crm/public/favicon.svg +1 -0
  211. package/corpus/templates/crm/public/icon-180.svg +1 -0
  212. package/corpus/templates/crm/public/manifest.json +11 -0
  213. package/corpus/templates/crm/react-router.config.ts +7 -0
  214. package/corpus/templates/crm/server/crm/adapter.ts +32 -0
  215. package/corpus/templates/crm/server/crm/crm-field-firewall.ts +64 -0
  216. package/corpus/templates/crm/server/crm/crm-mirror.ts +721 -0
  217. package/corpus/templates/crm/server/crm/field-policy-attributes.ts +48 -0
  218. package/corpus/templates/crm/server/crm/hubspot-adapter.ts +1146 -0
  219. package/corpus/templates/crm/server/crm/native-adapter.ts +1810 -0
  220. package/corpus/templates/crm/server/crm/provider-record-link.ts +264 -0
  221. package/corpus/templates/crm/server/crm/read-through.ts +207 -0
  222. package/corpus/templates/crm/server/crm/salesforce-adapter.ts +1255 -0
  223. package/corpus/templates/crm/server/db/crm-store.ts +1112 -0
  224. package/corpus/templates/crm/server/db/index.ts +191 -0
  225. package/corpus/templates/crm/server/db/schema.ts +647 -0
  226. package/corpus/templates/crm/server/lib/crm-query.ts +1558 -0
  227. package/corpus/templates/crm/server/lib/dedupe.ts +714 -0
  228. package/corpus/templates/crm/server/lib/enrichment-cost.ts +381 -0
  229. package/corpus/templates/crm/server/lib/enrichment-slots.ts +519 -0
  230. package/corpus/templates/crm/server/lib/intelligence/default-detectors.ts +65 -0
  231. package/corpus/templates/crm/server/lib/intelligence/evidence.ts +105 -0
  232. package/corpus/templates/crm/server/lib/intelligence/keyword-detector.ts +94 -0
  233. package/corpus/templates/crm/server/lib/intelligence/smart-detector.ts +39 -0
  234. package/corpus/templates/crm/server/lib/intelligence/summary.ts +19 -0
  235. package/corpus/templates/crm/server/lib/lifecycle.ts +634 -0
  236. package/corpus/templates/crm/server/lib/provider-api.ts +64 -0
  237. package/corpus/templates/crm/server/middleware/auth.ts +4 -0
  238. package/corpus/templates/crm/server/plugins/agent-chat.ts +65 -0
  239. package/corpus/templates/crm/server/plugins/auth.ts +14 -0
  240. package/corpus/templates/crm/server/plugins/core-routes.ts +17 -0
  241. package/corpus/templates/crm/server/plugins/db.ts +738 -0
  242. package/corpus/templates/crm/server/routes/[...page].get.ts +5 -0
  243. package/corpus/templates/crm/shared/crm-attributes.ts +469 -0
  244. package/corpus/templates/crm/shared/crm-automation-recipes.ts +132 -0
  245. package/corpus/templates/crm/shared/crm-contract.ts +417 -0
  246. package/corpus/templates/crm/shared/crm-navigation.ts +190 -0
  247. package/corpus/templates/crm/shared/crm-sales-config.ts +63 -0
  248. package/corpus/templates/crm/ssr-entry.ts +11 -0
  249. package/corpus/templates/crm/tsconfig.json +24 -0
  250. package/corpus/templates/crm/vite.config.ts +15 -0
  251. package/corpus/templates/crm/vitest.config.ts +5 -0
  252. package/corpus/templates/design/app/i18n-data.ts +12 -12
  253. package/corpus/templates/design/changelog/2026-07-26-apply-design-updates-from-a-centered-top-toolbar-without-cov.md +6 -0
  254. package/dist/cli/templates-meta.d.ts.map +1 -1
  255. package/dist/cli/templates-meta.js +12 -0
  256. package/dist/cli/templates-meta.js.map +1 -1
  257. package/dist/collab/routes.d.ts +1 -1
  258. package/dist/observability/routes.d.ts +3 -3
  259. package/dist/resources/handlers.d.ts +1 -1
  260. package/dist/server/realtime-token.d.ts +1 -1
  261. package/dist/server/transcribe-voice.d.ts +1 -1
  262. package/package.json +1 -1
  263. package/src/cli/templates-meta.ts +12 -0
@@ -0,0 +1,364 @@
1
+ import { defineAction, type ActionRunContext } from "@agent-native/core/action";
2
+ import { accessFilter } from "@agent-native/core/sharing";
3
+ import { and, desc, eq } from "drizzle-orm";
4
+ import { z } from "zod";
5
+
6
+ import { getDb, schema } from "../server/db/index.js";
7
+ import { requireCrmScope } from "./_crm-action-utils.js";
8
+
9
+ /**
10
+ * Everything here is derived from the auth session — there is no per-user CRM
11
+ * configuration to author, and none should be introduced. "Who am I in this
12
+ * CRM" is answered from the mirrored owner attribution on the records the caller
13
+ * already owns.
14
+ */
15
+
16
+ /** Records scanned to build the book of business before the answer is partial. */
17
+ const MAX_BOOK_SCAN = 500;
18
+ const MAX_TASK_SCAN = 200;
19
+ const MAX_QUEUE_SCAN = 100;
20
+
21
+ export type CrmOwnerStatus =
22
+ | "resolved"
23
+ | "unmapped"
24
+ | "ambiguous"
25
+ | "unreadable"
26
+ | "not-applicable";
27
+
28
+ interface OwnerError {
29
+ code: "crm-owner-ambiguous" | "crm-owner-connection-unhealthy";
30
+ message: string;
31
+ connectionId?: string;
32
+ }
33
+
34
+ interface OwnedRecord {
35
+ id: string;
36
+ displayName: string;
37
+ objectType: string;
38
+ kind: string;
39
+ stage: string | null;
40
+ amount: number | null;
41
+ currencyCode: string | null;
42
+ closeDate: string | null;
43
+ connectionId: string;
44
+ ownerRemoteId: string | null;
45
+ ownerName: string | null;
46
+ nextContactAt: string | null;
47
+ updatedAt: string;
48
+ }
49
+
50
+ interface ConnectionHealth {
51
+ id: string;
52
+ provider: string;
53
+ label: string;
54
+ mode: string;
55
+ status: string;
56
+ lastSyncedAt: string | null;
57
+ lastError: string | null;
58
+ }
59
+
60
+ /**
61
+ * Resolve the caller's provider owner identity from their own mirrored records.
62
+ *
63
+ * The four non-resolved outcomes are deliberately DIFFERENT values. "No provider
64
+ * connection", "connected but nothing attributes you to an owner", "the mirror
65
+ * is stale because a connection is broken", and "you appear under two owner ids"
66
+ * all render as an empty owner slot if they are flattened, and the third one is
67
+ * the dangerous case: it would let a broken connection look exactly like an
68
+ * empty book of business.
69
+ */
70
+ function resolveWorkspaceOwner(input: {
71
+ connections: ConnectionHealth[];
72
+ records: OwnedRecord[];
73
+ }): {
74
+ ownerStatus: CrmOwnerStatus;
75
+ owner: {
76
+ connectionId: string;
77
+ provider: string;
78
+ ownerRemoteId: string;
79
+ ownerName: string | null;
80
+ recordCount: number;
81
+ } | null;
82
+ ownerError?: OwnerError;
83
+ } {
84
+ const providerConnections = input.connections.filter(
85
+ (connection) => connection.provider !== "native",
86
+ );
87
+ if (!providerConnections.length) {
88
+ return { ownerStatus: "not-applicable", owner: null };
89
+ }
90
+
91
+ const unhealthy = providerConnections.find(
92
+ (connection) =>
93
+ connection.status === "error" || connection.status === "disconnected",
94
+ );
95
+ if (unhealthy) {
96
+ return {
97
+ ownerStatus: "unreadable",
98
+ owner: null,
99
+ ownerError: {
100
+ code: "crm-owner-connection-unhealthy",
101
+ message: `Connection "${unhealthy.label}" is ${unhealthy.status}${
102
+ unhealthy.lastError ? `: ${unhealthy.lastError}` : ""
103
+ }. Your provider owner identity cannot be determined from a stale mirror — reconnect it before treating this book of business as complete.`,
104
+ connectionId: unhealthy.id,
105
+ },
106
+ };
107
+ }
108
+
109
+ const byOwner = new Map<
110
+ string,
111
+ {
112
+ connectionId: string;
113
+ ownerRemoteId: string;
114
+ ownerName: string | null;
115
+ count: number;
116
+ }
117
+ >();
118
+ for (const record of input.records) {
119
+ if (!record.ownerRemoteId) continue;
120
+ const key = `${record.connectionId}:${record.ownerRemoteId}`;
121
+ const bucket = byOwner.get(key) ?? {
122
+ connectionId: record.connectionId,
123
+ ownerRemoteId: record.ownerRemoteId,
124
+ ownerName: record.ownerName,
125
+ count: 0,
126
+ };
127
+ bucket.count += 1;
128
+ byOwner.set(key, bucket);
129
+ }
130
+
131
+ const owners = [...byOwner.values()].sort((a, b) => b.count - a.count);
132
+ if (!owners.length) {
133
+ return { ownerStatus: "unmapped", owner: null };
134
+ }
135
+ if (owners.length > 1) {
136
+ return {
137
+ ownerStatus: "ambiguous",
138
+ owner: null,
139
+ ownerError: {
140
+ code: "crm-owner-ambiguous",
141
+ message: `Your records are attributed to ${owners.length} different provider owner ids (${owners
142
+ .map((owner) => owner.ownerRemoteId)
143
+ .join(
144
+ ", ",
145
+ )}). Pick the right one in the provider before relying on owner-scoped work.`,
146
+ },
147
+ };
148
+ }
149
+
150
+ const [owner] = owners;
151
+ return {
152
+ ownerStatus: "resolved",
153
+ owner: {
154
+ connectionId: owner.connectionId,
155
+ provider:
156
+ providerConnections.find(
157
+ (connection) => connection.id === owner.connectionId,
158
+ )?.provider ?? "custom",
159
+ ownerRemoteId: owner.ownerRemoteId,
160
+ ownerName: owner.ownerName,
161
+ recordCount: owner.count,
162
+ },
163
+ };
164
+ }
165
+
166
+ function bucketTaskDue(
167
+ dueAt: string | null,
168
+ now: number,
169
+ ): "overdue" | "today" | "upcoming" | "undated" {
170
+ if (!dueAt) return "undated";
171
+ const due = Date.parse(dueAt);
172
+ if (!Number.isFinite(due)) return "undated";
173
+ if (due < now) return "overdue";
174
+ return due - now <= 24 * 60 * 60 * 1000 ? "today" : "upcoming";
175
+ }
176
+
177
+ export default defineAction({
178
+ description:
179
+ "One-call orientation for the caller: who they are, their provider owner identity, their book of business by stage, follow-up tasks due, pending write proposals, unreviewed signals, and connection health. Everything is derived from the signed-in session with no setup. Call this first when starting CRM work. A provider owner that cannot be determined comes back as ownerStatus plus ownerError — never as an empty book.",
180
+ schema: z.object({
181
+ bookLimit: z.coerce
182
+ .number()
183
+ .int()
184
+ .min(1)
185
+ .max(50)
186
+ .default(10)
187
+ .describe("How many of the caller's records to list in full."),
188
+ }),
189
+ http: { method: "GET" },
190
+ readOnly: true,
191
+ run: async (args, ctx?: ActionRunContext) => {
192
+ const scope = requireCrmScope(ctx);
193
+ const db = getDb();
194
+ const now = Date.now();
195
+
196
+ const connections: ConnectionHealth[] = await db
197
+ .select({
198
+ id: schema.crmConnections.id,
199
+ provider: schema.crmConnections.provider,
200
+ label: schema.crmConnections.label,
201
+ mode: schema.crmConnections.mode,
202
+ status: schema.crmConnections.status,
203
+ lastSyncedAt: schema.crmConnections.lastSyncedAt,
204
+ lastError: schema.crmConnections.lastError,
205
+ })
206
+ .from(schema.crmConnections)
207
+ .where(accessFilter(schema.crmConnections, schema.crmConnectionShares))
208
+ .limit(MAX_QUEUE_SCAN);
209
+
210
+ // The book of business is what the caller OWNS, not everything they can
211
+ // see — a shared org-wide record is somebody else's work.
212
+ const records: OwnedRecord[] = await db
213
+ .select({
214
+ id: schema.crmRecords.id,
215
+ displayName: schema.crmRecords.displayName,
216
+ objectType: schema.crmRecords.objectType,
217
+ kind: schema.crmRecords.kind,
218
+ stage: schema.crmRecords.stage,
219
+ amount: schema.crmRecords.amount,
220
+ currencyCode: schema.crmRecords.currencyCode,
221
+ closeDate: schema.crmRecords.closeDate,
222
+ connectionId: schema.crmRecords.connectionId,
223
+ ownerRemoteId: schema.crmRecords.ownerRemoteId,
224
+ ownerName: schema.crmRecords.ownerName,
225
+ nextContactAt: schema.crmRecords.nextContactAt,
226
+ updatedAt: schema.crmRecords.updatedAt,
227
+ })
228
+ .from(schema.crmRecords)
229
+ .where(
230
+ and(
231
+ eq(schema.crmRecords.ownerEmail, scope.ownerEmail),
232
+ eq(schema.crmRecords.tombstone, false),
233
+ accessFilter(schema.crmRecords, schema.crmRecordShares),
234
+ ),
235
+ )
236
+ .orderBy(desc(schema.crmRecords.updatedAt))
237
+ .limit(MAX_BOOK_SCAN + 1);
238
+
239
+ // A truncated scan is not a completed one: the counts below would be a
240
+ // confident understatement, so say so instead of rounding it off.
241
+ const bookComplete = records.length <= MAX_BOOK_SCAN;
242
+ const scanned = bookComplete ? records : records.slice(0, MAX_BOOK_SCAN);
243
+
244
+ const byStage: Record<string, { records: number; amount: number }> = {};
245
+ const byKind: Record<string, number> = {};
246
+ for (const record of scanned) {
247
+ const stage = record.stage ?? "(unstaged)";
248
+ const bucket = byStage[stage] ?? { records: 0, amount: 0 };
249
+ bucket.records += 1;
250
+ if (record.kind === "opportunity" && typeof record.amount === "number") {
251
+ bucket.amount += record.amount;
252
+ }
253
+ byStage[stage] = bucket;
254
+ byKind[record.kind] = (byKind[record.kind] ?? 0) + 1;
255
+ }
256
+
257
+ const taskRows = await db
258
+ .select({
259
+ id: schema.crmTasks.id,
260
+ recordId: schema.crmTasks.recordId,
261
+ title: schema.crmTasks.title,
262
+ dueAt: schema.crmTasks.dueAt,
263
+ assignedTo: schema.crmTasks.assignedTo,
264
+ })
265
+ .from(schema.crmTasks)
266
+ .where(
267
+ and(
268
+ eq(schema.crmTasks.status, "open"),
269
+ accessFilter(schema.crmTasks, schema.crmTaskShares),
270
+ ),
271
+ )
272
+ .orderBy(desc(schema.crmTasks.updatedAt))
273
+ .limit(MAX_TASK_SCAN);
274
+ const taskCounts = { overdue: 0, today: 0, upcoming: 0, undated: 0 };
275
+ for (const task of taskRows) {
276
+ taskCounts[bucketTaskDue(task.dueAt, now)] += 1;
277
+ }
278
+
279
+ const proposals = await db
280
+ .select({ id: schema.crmMutations.id })
281
+ .from(schema.crmMutations)
282
+ .where(
283
+ and(
284
+ eq(schema.crmMutations.status, "pending"),
285
+ accessFilter(schema.crmMutations, schema.crmMutationShares),
286
+ ),
287
+ )
288
+ .limit(MAX_QUEUE_SCAN);
289
+
290
+ const signals = await db
291
+ .select({ id: schema.crmSignals.id })
292
+ .from(schema.crmSignals)
293
+ .where(
294
+ and(
295
+ eq(schema.crmSignals.reviewStatus, "unreviewed"),
296
+ accessFilter(schema.crmSignals, schema.crmSignalShares),
297
+ ),
298
+ )
299
+ .limit(MAX_QUEUE_SCAN);
300
+
301
+ const ownerResolution = resolveWorkspaceOwner({
302
+ connections,
303
+ records: scanned,
304
+ });
305
+
306
+ return {
307
+ user: {
308
+ email: scope.ownerEmail,
309
+ orgId: scope.orgId,
310
+ visibility: scope.visibility,
311
+ },
312
+ ...ownerResolution,
313
+ book: {
314
+ recordCount: scanned.length,
315
+ complete: bookComplete,
316
+ scanLimit: MAX_BOOK_SCAN,
317
+ byStage,
318
+ byKind,
319
+ needsAttention: scanned
320
+ .filter(
321
+ (record) =>
322
+ record.nextContactAt !== null &&
323
+ Date.parse(record.nextContactAt) <= now,
324
+ )
325
+ .slice(0, args.bookLimit)
326
+ .map((record) => ({
327
+ id: record.id,
328
+ displayName: record.displayName,
329
+ stage: record.stage,
330
+ nextContactAt: record.nextContactAt,
331
+ })),
332
+ records: scanned.slice(0, args.bookLimit).map((record) => ({
333
+ id: record.id,
334
+ displayName: record.displayName,
335
+ objectType: record.objectType,
336
+ kind: record.kind,
337
+ stage: record.stage,
338
+ amount: record.amount,
339
+ currencyCode: record.currencyCode,
340
+ closeDate: record.closeDate,
341
+ updatedAt: record.updatedAt,
342
+ })),
343
+ },
344
+ tasks: {
345
+ open: taskRows.length,
346
+ complete: taskRows.length < MAX_TASK_SCAN,
347
+ ...taskCounts,
348
+ next: taskRows
349
+ .filter((task) => task.dueAt !== null)
350
+ .sort((a, b) => (a.dueAt ?? "").localeCompare(b.dueAt ?? ""))
351
+ .slice(0, 5),
352
+ },
353
+ proposals: {
354
+ pending: proposals.length,
355
+ complete: proposals.length < MAX_QUEUE_SCAN,
356
+ },
357
+ signals: {
358
+ unreviewed: signals.length,
359
+ complete: signals.length < MAX_QUEUE_SCAN,
360
+ },
361
+ connections,
362
+ };
363
+ },
364
+ });
@@ -0,0 +1,81 @@
1
+ import { defineAction, type ActionRunContext } from "@agent-native/core/action";
2
+ import { upsertDataProgram } from "@agent-native/core/data-programs";
3
+ import { z } from "zod";
4
+
5
+ import { crmDashboardStore } from "../server/db/index.js";
6
+ import { CRM_APP_ID } from "../server/lib/provider-api.js";
7
+ import {
8
+ createPipelineDashboardConfig,
9
+ crmPipelineDashboardId,
10
+ CRM_PIPELINE_PROGRAM_CODE,
11
+ CRM_PIPELINE_PROGRAM_COLUMNS,
12
+ CRM_PIPELINE_PROGRAM_NAME,
13
+ CRM_PIPELINE_PROGRAM_TITLE,
14
+ CRM_PIPELINE_REFRESH_TTL_MS,
15
+ CRM_PIPELINE_DASHBOARD_TITLE,
16
+ requireDashboardAccess,
17
+ } from "./_crm-dashboard.js";
18
+ import getCrmPipelineData from "./get-crm-pipeline-data.js";
19
+
20
+ export default defineAction({
21
+ description:
22
+ "Install or update your CRM Pipeline dashboard and its owned data program. Safe to rerun: it verifies the access-scoped source first, upserts the program by owner and name, then revision-writes the dashboard.",
23
+ schema: z.object({}),
24
+ run: async (_args, ctx?: ActionRunContext) => {
25
+ const access = requireDashboardAccess(ctx);
26
+ const preview = await getCrmPipelineData.run({}, ctx);
27
+
28
+ const program = await upsertDataProgram({
29
+ appId: CRM_APP_ID,
30
+ name: CRM_PIPELINE_PROGRAM_NAME,
31
+ title: CRM_PIPELINE_PROGRAM_TITLE,
32
+ description:
33
+ "Access-scoped opportunity totals by stage for the CRM Pipeline dashboard.",
34
+ code: CRM_PIPELINE_PROGRAM_CODE,
35
+ outputColumns: JSON.stringify(CRM_PIPELINE_PROGRAM_COLUMNS),
36
+ refreshMode: "ttl",
37
+ refreshTtlMs: CRM_PIPELINE_REFRESH_TTL_MS,
38
+ background: false,
39
+ ownerEmail: access.userEmail,
40
+ orgId: access.orgId ?? null,
41
+ });
42
+ if (
43
+ program.appId !== CRM_APP_ID ||
44
+ program.name !== CRM_PIPELINE_PROGRAM_NAME
45
+ ) {
46
+ throw new Error(
47
+ "CRM Pipeline data program could not be verified after saving.",
48
+ );
49
+ }
50
+
51
+ const dashboardId = crmPipelineDashboardId(access);
52
+ const existing = await crmDashboardStore.get(dashboardId, access);
53
+ const dashboard = await crmDashboardStore.write(
54
+ {
55
+ id: dashboardId,
56
+ kind: "pipeline",
57
+ title: CRM_PIPELINE_DASHBOARD_TITLE,
58
+ config: createPipelineDashboardConfig(program.id),
59
+ ...(existing ? { expectedUpdatedAt: existing.updatedAt } : {}),
60
+ },
61
+ access,
62
+ );
63
+ if (dashboard.config.panels.length !== 3) {
64
+ throw new Error(
65
+ "CRM Pipeline dashboard could not be verified after saving.",
66
+ );
67
+ }
68
+
69
+ return {
70
+ dashboard,
71
+ dashboardId: dashboard.id,
72
+ programId: program.id,
73
+ created: !existing,
74
+ programPreview: {
75
+ rowCount: preview.rows.length,
76
+ columns: CRM_PIPELINE_PROGRAM_COLUMNS,
77
+ truncated: preview.truncated,
78
+ },
79
+ };
80
+ },
81
+ });
@@ -0,0 +1,71 @@
1
+ import { defineAction } from "@agent-native/core/action";
2
+ import { accessFilter } from "@agent-native/core/sharing";
3
+ import { and, asc, eq } from "drizzle-orm";
4
+ import { z } from "zod";
5
+
6
+ import { getDb, schema } from "../server/db/index.js";
7
+ import { queryFlag } from "./_crm-action-utils.js";
8
+ import {
9
+ loadAttributeOptions,
10
+ toAttributeDefinition,
11
+ } from "./_crm-attribute-utils.js";
12
+
13
+ export default defineAction({
14
+ description:
15
+ "List the typed attributes (schema) of one CRM object type or list, with their managed status/select options. Use this before reading, writing, filtering, or grouping record values so slugs, types, cardinality, and option values are exact. Archived attributes are excluded unless includeArchived is set.",
16
+ schema: z.object({
17
+ target: z.enum(["object", "list"]).default("object"),
18
+ targetId: z
19
+ .string()
20
+ .trim()
21
+ .min(1)
22
+ .max(200)
23
+ .describe(
24
+ "The object type for target=object (for example accounts), or the list id for target=list.",
25
+ ),
26
+ connectionId: z.string().trim().min(1).max(128).optional(),
27
+ includeArchived: queryFlag,
28
+ limit: z.coerce.number().int().min(1).max(500).default(200),
29
+ }),
30
+ http: { method: "GET" },
31
+ readOnly: true,
32
+ run: async (args) => {
33
+ const rows = await getDb()
34
+ .select()
35
+ .from(schema.crmFieldPolicies)
36
+ .where(
37
+ and(
38
+ eq(schema.crmFieldPolicies.target, args.target),
39
+ // `object_type` carries the target for both kinds — for list
40
+ // attributes it mirrors `target_id` — and, unlike `target_id`, it is
41
+ // populated on rows written by the provider adapters.
42
+ eq(schema.crmFieldPolicies.objectType, args.targetId),
43
+ ...(args.connectionId
44
+ ? [eq(schema.crmFieldPolicies.connectionId, args.connectionId)]
45
+ : []),
46
+ ...(args.includeArchived
47
+ ? []
48
+ : [eq(schema.crmFieldPolicies.archived, false)]),
49
+ accessFilter(schema.crmFieldPolicies, schema.crmFieldPolicyShares),
50
+ ),
51
+ )
52
+ .orderBy(
53
+ asc(schema.crmFieldPolicies.position),
54
+ asc(schema.crmFieldPolicies.label),
55
+ )
56
+ .limit(args.limit);
57
+
58
+ const options = await loadAttributeOptions({
59
+ attributeIds: rows.map((row) => row.id),
60
+ includeArchived: args.includeArchived,
61
+ });
62
+
63
+ return {
64
+ target: args.target,
65
+ targetId: args.targetId,
66
+ attributes: rows.map((row) =>
67
+ toAttributeDefinition(row, options.get(row.id) ?? []),
68
+ ),
69
+ };
70
+ },
71
+ });
@@ -0,0 +1,68 @@
1
+ import { defineAction } from "@agent-native/core/action";
2
+ import { accessFilter } from "@agent-native/core/sharing";
3
+ import { desc } from "drizzle-orm";
4
+ import { z } from "zod";
5
+
6
+ import { getDb, schema } from "../server/db/index.js";
7
+
8
+ /**
9
+ * The object types a connection mirrors. An unreadable blob is not reported as
10
+ * "mirrors nothing": the settings UI would then render an empty target picker,
11
+ * which is indistinguishable from a correctly configured CRM that happens to
12
+ * have no objects.
13
+ */
14
+ function parseObjectTypes(connectionId: string, raw: string): string[] {
15
+ let parsed: unknown;
16
+ try {
17
+ parsed = JSON.parse(raw);
18
+ } catch {
19
+ throw new Error(
20
+ `CRM connection ${connectionId} has an unreadable selected_object_types_json. Repair it before reading this connection.`,
21
+ );
22
+ }
23
+ if (
24
+ !Array.isArray(parsed) ||
25
+ parsed.some((value) => typeof value !== "string")
26
+ ) {
27
+ throw new Error(
28
+ `CRM connection ${connectionId} has a selected_object_types_json that is not a list of object types.`,
29
+ );
30
+ }
31
+ return parsed as string[];
32
+ }
33
+
34
+ export default defineAction({
35
+ description:
36
+ "List the access-scoped CRM connections with their mode, health, and mirrored object types. Use it to discover which object types a connection carries before reading or authoring attributes on them. The mode `hybrid` is deprecated and only ever appears on connections configured before per-attribute authority replaced it.",
37
+ schema: z.object({
38
+ limit: z.coerce.number().int().min(1).max(50).default(20),
39
+ }),
40
+ http: { method: "GET" },
41
+ readOnly: true,
42
+ run: async (args) => {
43
+ const rows = await getDb()
44
+ .select({
45
+ id: schema.crmConnections.id,
46
+ provider: schema.crmConnections.provider,
47
+ label: schema.crmConnections.label,
48
+ accountId: schema.crmConnections.accountId,
49
+ mode: schema.crmConnections.mode,
50
+ status: schema.crmConnections.status,
51
+ selectedObjectTypesJson: schema.crmConnections.selectedObjectTypesJson,
52
+ lastSyncedAt: schema.crmConnections.lastSyncedAt,
53
+ lastError: schema.crmConnections.lastError,
54
+ updatedAt: schema.crmConnections.updatedAt,
55
+ })
56
+ .from(schema.crmConnections)
57
+ .where(accessFilter(schema.crmConnections, schema.crmConnectionShares))
58
+ .orderBy(desc(schema.crmConnections.updatedAt))
59
+ .limit(args.limit);
60
+
61
+ return {
62
+ connections: rows.map(({ selectedObjectTypesJson, ...row }) => ({
63
+ ...row,
64
+ objectTypes: parseObjectTypes(row.id, selectedObjectTypesJson),
65
+ })),
66
+ };
67
+ },
68
+ });
@@ -0,0 +1,15 @@
1
+ import { defineAction, type ActionRunContext } from "@agent-native/core/action";
2
+ import { z } from "zod";
3
+
4
+ import { crmDashboardStore } from "../server/db/index.js";
5
+ import { requireDashboardAccess } from "./_crm-dashboard.js";
6
+
7
+ export default defineAction({
8
+ description:
9
+ "List the bounded revision history for one access-scoped CRM dashboard.",
10
+ schema: z.object({ id: z.string().trim().min(1).max(200) }),
11
+ http: { method: "GET" },
12
+ readOnly: true,
13
+ run: ({ id }, ctx?: ActionRunContext) =>
14
+ crmDashboardStore.listRevisions(id, requireDashboardAccess(ctx)),
15
+ });
@@ -0,0 +1,15 @@
1
+ import { defineAction, type ActionRunContext } from "@agent-native/core/action";
2
+ import { z } from "zod";
3
+
4
+ import { crmDashboardStore } from "../server/db/index.js";
5
+ import { requireDashboardAccess } from "./_crm-dashboard.js";
6
+
7
+ export default defineAction({
8
+ description:
9
+ "List up to 50 active CRM dashboards the current user can access.",
10
+ schema: z.object({}),
11
+ http: { method: "GET" },
12
+ readOnly: true,
13
+ run: async (_args, ctx?: ActionRunContext) =>
14
+ (await crmDashboardStore.list(requireDashboardAccess(ctx))).slice(0, 50),
15
+ });
@@ -0,0 +1,28 @@
1
+ import { defineAction } from "@agent-native/core/action";
2
+ import { z } from "zod";
3
+
4
+ import {
5
+ CRM_ENRICHMENT_SLOT_UNIT_COST,
6
+ resolveEnrichmentBudgetUnits,
7
+ } from "../server/lib/enrichment-cost.js";
8
+ import { describeEnrichmentSlots } from "../server/lib/enrichment-slots.js";
9
+ import { requireCrmScope } from "./_crm-action-utils.js";
10
+
11
+ export default defineAction({
12
+ description:
13
+ "List the CRM enrichment slots with their credential state and unit price. A slot is a capability (company, person, contact, web, calls), not a vendor — which provider backs a slot is an internal binding and never appears here. Each slot reports credential.status as granted, missing (with a reason), or unknown (the lookup itself failed, which is NOT the same as missing). Slots in the verify phase are free-standing evidence gathering; the spend phase slots carry contact data and are the ones the approval gate protects. Call this before estimate-crm-enrichment to see which slots a run can actually use.",
14
+ schema: z.object({}),
15
+ http: { method: "GET" },
16
+ readOnly: true,
17
+ run: async (_args, ctx) => {
18
+ requireCrmScope(ctx);
19
+ const slots = await describeEnrichmentSlots();
20
+ return {
21
+ budgetUnitsPerPeriod: resolveEnrichmentBudgetUnits(),
22
+ slots: slots.map((slot) => ({
23
+ ...slot,
24
+ unitCost: CRM_ENRICHMENT_SLOT_UNIT_COST[slot.slot],
25
+ })),
26
+ };
27
+ },
28
+ });