@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,647 @@
1
+ import { defineAction, type ActionRunContext } from "@agent-native/core/action";
2
+ import { accessFilter } from "@agent-native/core/sharing";
3
+ import { and, eq, inArray } from "drizzle-orm";
4
+ import { z } from "zod";
5
+
6
+ import { getDb, schema } from "../server/db/index.js";
7
+ import {
8
+ CrmEnrichmentScopeError,
9
+ actualEnrichmentCost,
10
+ assertNoInFlightRun,
11
+ assertRecordCountWithinCap,
12
+ assertWithinEnrichmentBudget,
13
+ buildPhaseBInput,
14
+ claimEnrichmentRun,
15
+ currentSpendPeriodStart,
16
+ estimateEnrichment,
17
+ evaluateEnrichmentBudget,
18
+ resolveEnrichmentBudgetUnits,
19
+ resolveEnrichmentScope,
20
+ type CrmEnrichmentScope,
21
+ type CrmVerifiedRecord,
22
+ } from "../server/lib/enrichment-cost.js";
23
+ import {
24
+ CRM_ENRICHMENT_SLOTS,
25
+ runEnrichmentSlots,
26
+ spendSlots,
27
+ verifySlots,
28
+ type CrmEnrichmentPhase,
29
+ type CrmEnrichmentSlot,
30
+ type CrmEnrichmentSlotDeps,
31
+ type CrmEnrichmentSlotOutcome,
32
+ type CrmEnrichmentTarget,
33
+ } from "../server/lib/enrichment-slots.js";
34
+ import {
35
+ writeCrmRecordField,
36
+ type CrmWritableAttribute,
37
+ } from "../server/lib/record-fields.js";
38
+ import { parsePersonalName } from "../shared/crm-attributes.js";
39
+ import { requireCrmScope, toJson } from "./_crm-action-utils.js";
40
+ import {
41
+ legacyValueTypeFor,
42
+ type CrmAttributeRow,
43
+ } from "./_crm-attribute-utils.js";
44
+ import {
45
+ resolveScopeRecordIds,
46
+ readEnrichmentSpendToDate,
47
+ } from "./estimate-crm-enrichment.js";
48
+ import { protectedBy, type CurrentFieldRow } from "./run-crm-attribute-fill.js";
49
+
50
+ /** Ceiling on the stored evidence blob. Past it the run stores summaries only. */
51
+ const MAX_OUTCOMES_CHARS = 200_000;
52
+
53
+ interface RecordOutcome {
54
+ recordId: string;
55
+ slots: CrmEnrichmentSlotOutcome[];
56
+ writes?: FactWrite[];
57
+ }
58
+
59
+ interface FactWrite {
60
+ factKey: string;
61
+ outcome: "written" | "unchanged" | "kept-existing" | "no-attribute";
62
+ keptBecause?: string;
63
+ }
64
+
65
+ export interface RunCrmEnrichmentArgs {
66
+ phase: CrmEnrichmentPhase;
67
+ scopeKind: "object" | "list" | "records";
68
+ targetId?: string;
69
+ recordIds?: string[];
70
+ slots?: CrmEnrichmentSlot[];
71
+ sourceRunId?: string;
72
+ approvedRecordIds?: string[];
73
+ }
74
+
75
+ /** Injectable provider substrate. Production leaves it unset. */
76
+ export interface EnrichmentDeps {
77
+ slots?: CrmEnrichmentSlotDeps;
78
+ }
79
+
80
+ export default defineAction({
81
+ description:
82
+ "Run one phase of a gated CRM enrichment. Phase verify is the free evidence pass: it never touches contact data, and its per-record evidence is what a human reviews. Phase spend is the paid pass and requires sourceRunId plus the explicit approvedRecordIds from that review — its input set is BUILT from those approvals, so an unapproved record is never visible to the paid job rather than merely filtered out. Both phases refuse to launch while another run for the same scope and phase is in flight, refuse to pass your period budget, and persist their run row with an atomic claim before any provider call so a crash mid-setup cannot leave a paid job nobody recorded. Values ingested by the paid pass MERGE: a human edit is kept, an unchanged value is not rewritten. Call estimate-crm-enrichment first and quote the cost.",
83
+ schema: z
84
+ .object({
85
+ phase: z.enum(["verify", "spend"]),
86
+ scopeKind: z.enum(["object", "list", "records"]).default("records"),
87
+ targetId: z.string().trim().min(1).max(200).optional(),
88
+ recordIds: z
89
+ .array(z.string().trim().min(1).max(128))
90
+ .max(2000)
91
+ .optional(),
92
+ slots: z.array(z.enum(CRM_ENRICHMENT_SLOTS)).min(1).optional(),
93
+ sourceRunId: z
94
+ .string()
95
+ .trim()
96
+ .min(1)
97
+ .max(128)
98
+ .optional()
99
+ .describe("Phase spend only: the completed verify run being approved."),
100
+ approvedRecordIds: z
101
+ .array(z.string().trim().min(1).max(128))
102
+ .max(2000)
103
+ .optional()
104
+ .describe(
105
+ "Phase spend only: the records the human approved. Nothing else is paid for.",
106
+ ),
107
+ })
108
+ .superRefine((value, ctx) => {
109
+ if (value.phase !== "spend") return;
110
+ if (!value.sourceRunId) {
111
+ ctx.addIssue({
112
+ code: "custom",
113
+ path: ["sourceRunId"],
114
+ message: "Phase spend requires the verify run it is approving.",
115
+ });
116
+ }
117
+ if (!value.approvedRecordIds?.length) {
118
+ ctx.addIssue({
119
+ code: "custom",
120
+ path: ["approvedRecordIds"],
121
+ message:
122
+ "Phase spend requires at least one approved record. The approval gate is not optional.",
123
+ });
124
+ }
125
+ }),
126
+ audit: {
127
+ summary: (args, result) => {
128
+ const run = result as { runId?: string; costUnits?: number };
129
+ return `Ran ${args.phase} CRM enrichment ${run.runId ?? ""} for ${run.costUnits ?? 0} units`;
130
+ },
131
+ },
132
+ run: (args, ctx?: ActionRunContext) => runCrmEnrichment(args, ctx),
133
+ });
134
+
135
+ /**
136
+ * The action body, callable with an injected provider substrate.
137
+ *
138
+ * `defineAction` wraps `run` and forwards only `(args, ctx)`, so an extra
139
+ * parameter on `run` itself is silently dropped. The seam has to live here or
140
+ * every test of the spend gate would have to reach a real provider to run.
141
+ */
142
+ export async function runCrmEnrichment(
143
+ args: RunCrmEnrichmentArgs,
144
+ ctx?: ActionRunContext,
145
+ deps?: EnrichmentDeps,
146
+ ) {
147
+ const ownership = requireCrmScope(ctx);
148
+ const requestedSlots = (args.slots ??
149
+ CRM_ENRICHMENT_SLOTS) as CrmEnrichmentSlot[];
150
+
151
+ const plan =
152
+ args.phase === "verify"
153
+ ? await planVerify(args, requestedSlots)
154
+ : await planSpend(args);
155
+
156
+ assertRecordCountWithinCap(plan.recordIds.length);
157
+ const estimate = estimateEnrichment({
158
+ phase: args.phase,
159
+ slots: requestedSlots,
160
+ recordCount: plan.recordIds.length,
161
+ });
162
+
163
+ const periodStart = currentSpendPeriodStart();
164
+ const budget = evaluateEnrichmentBudget({
165
+ estimatedUnits: estimate.totalCost,
166
+ spendToDate: await readEnrichmentSpendToDate({
167
+ ownerEmail: ownership.ownerEmail,
168
+ periodStart,
169
+ }),
170
+ capUnits: resolveEnrichmentBudgetUnits(),
171
+ periodStart,
172
+ });
173
+ assertWithinEnrichmentBudget(budget);
174
+
175
+ const db = getDb();
176
+ // Re-clicking must not create a second paid job for the same scope.
177
+ const inFlight = await db
178
+ .select({ id: schema.crmEnrichmentRuns.id })
179
+ .from(schema.crmEnrichmentRuns)
180
+ .where(
181
+ and(
182
+ eq(schema.crmEnrichmentRuns.scopeKind, plan.scope.kind),
183
+ eq(schema.crmEnrichmentRuns.scopeId, plan.scope.id),
184
+ eq(schema.crmEnrichmentRuns.phase, args.phase),
185
+ inArray(schema.crmEnrichmentRuns.status, ["queued", "running"]),
186
+ accessFilter(schema.crmEnrichmentRuns, schema.crmEnrichmentRunShares),
187
+ ),
188
+ )
189
+ .limit(1);
190
+ assertNoInFlightRun({ scope: plan.scope, phase: args.phase, inFlight });
191
+
192
+ // Persisted BEFORE any provider call: a crash between here and the call
193
+ // leaves a claimable row instead of spend nobody recorded. `slots` is the
194
+ // phase's own slot set, and `inputRecordIdsJson` is the frozen input — for
195
+ // phase spend it was constructed from the approvals and contains nothing else.
196
+ const runId = crypto.randomUUID();
197
+ const startedAt = new Date().toISOString();
198
+ await db.insert(schema.crmEnrichmentRuns).values({
199
+ id: runId,
200
+ scopeKind: plan.scope.kind,
201
+ scopeId: plan.scope.id,
202
+ phase: args.phase,
203
+ status: "queued",
204
+ sourceRunId: args.sourceRunId ?? null,
205
+ slotsJson: toJson(estimate.slots, 2000),
206
+ inputRecordIdsJson: toJson(plan.recordIds, 200_000),
207
+ estimateJson: toJson(estimate, 20_000),
208
+ startedAt,
209
+ createdAt: startedAt,
210
+ updatedAt: startedAt,
211
+ ...ownership,
212
+ });
213
+
214
+ const nonce = crypto.randomUUID();
215
+ const won = await claimEnrichmentRun({
216
+ nonce,
217
+ write: async (value) => {
218
+ await db
219
+ .update(schema.crmEnrichmentRuns)
220
+ .set({ claimNonce: value, claimedAt: new Date().toISOString() })
221
+ .where(
222
+ and(
223
+ eq(schema.crmEnrichmentRuns.id, runId),
224
+ eq(schema.crmEnrichmentRuns.status, "queued"),
225
+ accessFilter(
226
+ schema.crmEnrichmentRuns,
227
+ schema.crmEnrichmentRunShares,
228
+ undefined,
229
+ "editor",
230
+ ),
231
+ ),
232
+ );
233
+ },
234
+ readBack: async () => {
235
+ const [row] = await db
236
+ .select({ claimNonce: schema.crmEnrichmentRuns.claimNonce })
237
+ .from(schema.crmEnrichmentRuns)
238
+ .where(eq(schema.crmEnrichmentRuns.id, runId))
239
+ .limit(1);
240
+ return row?.claimNonce ?? null;
241
+ },
242
+ });
243
+ if (!won) {
244
+ // Somebody else owns this row's provider calls. Report it rather than
245
+ // running anyway — a lost claim is exactly what prevents double spend.
246
+ return {
247
+ runId,
248
+ phase: args.phase,
249
+ status: "queued" as const,
250
+ claimed: false,
251
+ estimate,
252
+ budget,
253
+ recordCount: plan.recordIds.length,
254
+ outcomes: [] as RecordOutcome[],
255
+ };
256
+ }
257
+
258
+ await db
259
+ .update(schema.crmEnrichmentRuns)
260
+ .set({ status: "running", updatedAt: new Date().toISOString() })
261
+ .where(eq(schema.crmEnrichmentRuns.id, runId));
262
+
263
+ const phaseSlots =
264
+ args.phase === "verify"
265
+ ? verifySlots(requestedSlots)
266
+ : spendSlots(requestedSlots);
267
+
268
+ const outcomes: RecordOutcome[] = [];
269
+ for (const target of plan.targets) {
270
+ const slotOutcomes = await runEnrichmentSlots({
271
+ slots: phaseSlots,
272
+ target,
273
+ phase: args.phase,
274
+ deps: deps?.slots,
275
+ });
276
+ outcomes.push({
277
+ recordId: target.recordId,
278
+ slots: slotOutcomes,
279
+ // Only the paid pass ingests values; the verify pass produces evidence
280
+ // for a human to approve and writes nothing to the record.
281
+ ...(args.phase === "spend"
282
+ ? {
283
+ writes: await ingestFacts({
284
+ recordId: target.recordId,
285
+ outcomes: slotOutcomes,
286
+ ownership,
287
+ actorId: ctx?.userEmail ?? ownership.ownerEmail,
288
+ }),
289
+ }
290
+ : {}),
291
+ });
292
+ }
293
+
294
+ const completedAt = new Date().toISOString();
295
+ const stored = storeableOutcomes(outcomes);
296
+ // Booked from what actually reached a provider, not from the quote: a slot
297
+ // that was unconfigured or skipped cost nothing, and period-to-date spend
298
+ // is summed from this column.
299
+ const costUnits = actualEnrichmentCost(outcomes);
300
+ await db
301
+ .update(schema.crmEnrichmentRuns)
302
+ .set({
303
+ status: "completed",
304
+ outcomesJson: stored.json,
305
+ costUnits,
306
+ completedAt,
307
+ updatedAt: completedAt,
308
+ })
309
+ .where(eq(schema.crmEnrichmentRuns.id, runId));
310
+
311
+ return {
312
+ runId,
313
+ phase: args.phase,
314
+ status: "completed" as const,
315
+ claimed: true,
316
+ estimate,
317
+ budget,
318
+ costUnits,
319
+ recordCount: plan.recordIds.length,
320
+ evidenceStored: stored.kind,
321
+ outcomes,
322
+ };
323
+ }
324
+
325
+ interface RunPlan {
326
+ scope: CrmEnrichmentScope;
327
+ readonly recordIds: readonly string[];
328
+ readonly targets: readonly CrmEnrichmentTarget[];
329
+ }
330
+
331
+ async function planVerify(
332
+ args: {
333
+ scopeKind: "object" | "list" | "records";
334
+ targetId?: string;
335
+ recordIds?: string[];
336
+ },
337
+ _slots: CrmEnrichmentSlot[],
338
+ ): Promise<RunPlan> {
339
+ const recordIds = await resolveScopeRecordIds(args);
340
+ const scope = await resolveEnrichmentScope({
341
+ kind: args.scopeKind,
342
+ targetId: args.targetId,
343
+ recordIds,
344
+ });
345
+ return { scope, recordIds, targets: await loadTargets(recordIds) };
346
+ }
347
+
348
+ /**
349
+ * The paid pass's plan.
350
+ *
351
+ * The input set comes out of `buildPhaseBInput`, which is handed only the
352
+ * approved entries. An id the verify run never saw is refused rather than
353
+ * quietly enriched — an approval has to be an approval OF something.
354
+ */
355
+ async function planSpend(args: {
356
+ sourceRunId?: string;
357
+ approvedRecordIds?: string[];
358
+ }): Promise<RunPlan> {
359
+ const sourceRunId = args.sourceRunId!;
360
+ const [source] = await getDb()
361
+ .select()
362
+ .from(schema.crmEnrichmentRuns)
363
+ .where(
364
+ and(
365
+ eq(schema.crmEnrichmentRuns.id, sourceRunId),
366
+ accessFilter(schema.crmEnrichmentRuns, schema.crmEnrichmentRunShares),
367
+ ),
368
+ )
369
+ .limit(1);
370
+ if (!source) {
371
+ throw new CrmEnrichmentScopeError(
372
+ "crm-enrichment-source-not-found",
373
+ `Verify run ${sourceRunId} was not found or is not readable by you.`,
374
+ );
375
+ }
376
+ if (source.phase !== "verify" || source.status !== "completed") {
377
+ throw new CrmEnrichmentScopeError(
378
+ "crm-enrichment-source-not-approvable",
379
+ `Run ${sourceRunId} is a ${source.status} ${source.phase} run. Only a completed verify run can be approved for spend.`,
380
+ );
381
+ }
382
+
383
+ const verified = parseVerifiedRecords(source.id, source.outcomesJson);
384
+ const verifiedIds = new Set(verified.map((entry) => entry.recordId));
385
+ const approved = new Set(args.approvedRecordIds ?? []);
386
+ const unverified = [...approved].filter((id) => !verifiedIds.has(id));
387
+ if (unverified.length > 0) {
388
+ throw new CrmEnrichmentScopeError(
389
+ "crm-enrichment-approval-unverified",
390
+ `These records were not part of verify run ${sourceRunId}, so they cannot be approved from it: ${unverified.join(", ")}.`,
391
+ );
392
+ }
393
+
394
+ const targets = await loadTargets([...approved]);
395
+ const entries: CrmVerifiedRecord[] = verified.map((entry) => ({
396
+ target: targets.find((target) => target.recordId === entry.recordId)!,
397
+ outcomes: [],
398
+ approved: approved.has(entry.recordId),
399
+ }));
400
+ const input = buildPhaseBInput(entries.filter((entry) => entry.target));
401
+ if (input.recordIds.length === 0) {
402
+ throw new CrmEnrichmentScopeError(
403
+ "crm-enrichment-approval-empty",
404
+ `None of the approved records of verify run ${sourceRunId} are readable by you.`,
405
+ );
406
+ }
407
+ return {
408
+ scope: { kind: source.scopeKind, id: source.scopeId },
409
+ recordIds: input.recordIds,
410
+ targets: input.targets,
411
+ };
412
+ }
413
+
414
+ /** Record ids the verify run actually produced evidence for. */
415
+ function parseVerifiedRecords(
416
+ runId: string,
417
+ outcomesJson: string,
418
+ ): Array<{ recordId: string }> {
419
+ let parsed: unknown;
420
+ try {
421
+ parsed = JSON.parse(outcomesJson);
422
+ } catch {
423
+ throw new CrmEnrichmentScopeError(
424
+ "crm-enrichment-source-unreadable",
425
+ `Verify run ${runId} has unreadable evidence, so its approvals cannot be checked. Re-run the verification.`,
426
+ );
427
+ }
428
+ if (!Array.isArray(parsed)) {
429
+ throw new CrmEnrichmentScopeError(
430
+ "crm-enrichment-source-unreadable",
431
+ `Verify run ${runId} stored no reviewable evidence. Re-run the verification.`,
432
+ );
433
+ }
434
+ return parsed.flatMap((entry) =>
435
+ entry &&
436
+ typeof entry === "object" &&
437
+ typeof (entry as Record<string, unknown>).recordId === "string"
438
+ ? [{ recordId: (entry as { recordId: string }).recordId }]
439
+ : [],
440
+ );
441
+ }
442
+
443
+ async function loadTargets(
444
+ recordIds: string[],
445
+ ): Promise<CrmEnrichmentTarget[]> {
446
+ if (recordIds.length === 0) return [];
447
+ const rows = await getDb()
448
+ .select({
449
+ id: schema.crmRecords.id,
450
+ displayName: schema.crmRecords.displayName,
451
+ domain: schema.crmRecords.domain,
452
+ kind: schema.crmRecords.kind,
453
+ })
454
+ .from(schema.crmRecords)
455
+ .where(
456
+ and(
457
+ inArray(schema.crmRecords.id, recordIds),
458
+ eq(schema.crmRecords.tombstone, false),
459
+ accessFilter(schema.crmRecords, schema.crmRecordShares),
460
+ ),
461
+ );
462
+ return rows.map((row) => {
463
+ const name =
464
+ row.kind === "person"
465
+ ? parsePersonalName(row.displayName)
466
+ : { status: "absent" as const };
467
+ return {
468
+ recordId: row.id,
469
+ displayName: row.displayName,
470
+ domain: row.domain,
471
+ firstName: name.status === "parsed" ? name.first : null,
472
+ lastName: name.status === "parsed" ? name.last : null,
473
+ companyName: row.kind === "person" ? null : row.displayName,
474
+ };
475
+ });
476
+ }
477
+
478
+ /**
479
+ * Merge a record's bought facts into its attributes.
480
+ *
481
+ * A fact lands on the attribute whose api slug matches its key. Merge, never
482
+ * upsert: a value a human edited is kept and reported as such, an unchanged
483
+ * value is not rewritten (which is what keeps bitemporal history from
484
+ * exploding), and a fact with no matching attribute is reported as
485
+ * `no-attribute` rather than dropped.
486
+ */
487
+ async function ingestFacts(input: {
488
+ recordId: string;
489
+ outcomes: CrmEnrichmentSlotOutcome[];
490
+ ownership: {
491
+ ownerEmail: string;
492
+ orgId: string | null;
493
+ visibility: "private" | "org";
494
+ };
495
+ actorId: string;
496
+ }): Promise<FactWrite[]> {
497
+ const facts = input.outcomes.flatMap((outcome) =>
498
+ outcome.status === "ok" ? outcome.facts : [],
499
+ );
500
+ if (facts.length === 0) return [];
501
+
502
+ const db = getDb();
503
+ const slugs = [...new Set(facts.map((fact) => fact.key))];
504
+ const attributeRows = (await db
505
+ .select()
506
+ .from(schema.crmFieldPolicies)
507
+ .where(
508
+ and(
509
+ inArray(schema.crmFieldPolicies.fieldName, slugs),
510
+ eq(schema.crmFieldPolicies.archived, false),
511
+ accessFilter(
512
+ schema.crmFieldPolicies,
513
+ schema.crmFieldPolicyShares,
514
+ undefined,
515
+ "editor",
516
+ ),
517
+ ),
518
+ )) as CrmAttributeRow[];
519
+ const bySlug = new Map(
520
+ attributeRows.map((row) => [row.apiSlug ?? row.fieldName, row]),
521
+ );
522
+
523
+ const current = await loadCurrentFieldRows(input.recordId, slugs);
524
+ const now = new Date().toISOString();
525
+ const writes: FactWrite[] = [];
526
+
527
+ for (const fact of facts) {
528
+ const row = bySlug.get(fact.key);
529
+ if (!row || !isLocallyStored(row)) {
530
+ writes.push({ factKey: fact.key, outcome: "no-attribute" });
531
+ continue;
532
+ }
533
+ const existing = current.get(row.apiSlug ?? row.fieldName);
534
+ const kept = existing ? protectedBy(existing) : null;
535
+ if (kept === "human-edit") {
536
+ writes.push({
537
+ factKey: fact.key,
538
+ outcome: "kept-existing",
539
+ keptBecause: kept,
540
+ });
541
+ continue;
542
+ }
543
+
544
+ const attribute: CrmWritableAttribute = {
545
+ id: row.id,
546
+ apiSlug: row.apiSlug ?? row.fieldName,
547
+ attributeType: row.attributeType,
548
+ multi: row.multi,
549
+ historyTracked: row.historyTracked,
550
+ valueType: legacyValueTypeFor(row.attributeType, row.multi),
551
+ storagePolicy: row.storagePolicy as
552
+ | "mirrored"
553
+ | "derived-local"
554
+ | "local-authoritative",
555
+ fieldPolicyId: row.id,
556
+ };
557
+ const result = await writeCrmRecordField({
558
+ target: { recordId: input.recordId },
559
+ attribute,
560
+ value: fact.value,
561
+ // A provider bought this value; the agent only routed it.
562
+ actor: { type: "provider", id: input.actorId },
563
+ ownership: input.ownership,
564
+ provenanceJson: toJson(
565
+ [
566
+ {
567
+ fieldName: attribute.apiSlug,
568
+ provider: "enrichment",
569
+ // What makes a later free fill keep this value instead of clobbering it.
570
+ paid: true,
571
+ ...(fact.sourceUrl ? { sourceUrl: fact.sourceUrl } : {}),
572
+ observedAt: now,
573
+ },
574
+ ],
575
+ 4000,
576
+ ),
577
+ now,
578
+ });
579
+ writes.push({
580
+ factKey: fact.key,
581
+ outcome: result.changed ? "written" : "unchanged",
582
+ });
583
+ }
584
+ return writes;
585
+ }
586
+
587
+ function isLocallyStored(row: CrmAttributeRow): boolean {
588
+ return (
589
+ row.storagePolicy === "mirrored" ||
590
+ row.storagePolicy === "derived-local" ||
591
+ row.storagePolicy === "local-authoritative"
592
+ );
593
+ }
594
+
595
+ async function loadCurrentFieldRows(recordId: string, slugs: string[]) {
596
+ const rows = await getDb()
597
+ .select({
598
+ recordId: schema.crmRecordFields.recordId,
599
+ fieldName: schema.crmRecordFields.fieldName,
600
+ stringValue: schema.crmRecordFields.stringValue,
601
+ numberValue: schema.crmRecordFields.numberValue,
602
+ booleanValue: schema.crmRecordFields.booleanValue,
603
+ jsonValue: schema.crmRecordFields.jsonValue,
604
+ actorType: schema.crmRecordFields.actorType,
605
+ provenanceJson: schema.crmRecordFields.provenanceJson,
606
+ })
607
+ .from(schema.crmRecordFields)
608
+ .where(
609
+ and(
610
+ eq(schema.crmRecordFields.recordId, recordId),
611
+ inArray(schema.crmRecordFields.fieldName, slugs),
612
+ accessFilter(schema.crmRecordFields, schema.crmRecordFieldShares),
613
+ ),
614
+ );
615
+ return new Map<string, CurrentFieldRow>(
616
+ rows
617
+ .filter((row) => row.provenanceJson !== null)
618
+ .map((row) => [row.fieldName, row as CurrentFieldRow]),
619
+ );
620
+ }
621
+
622
+ /**
623
+ * The evidence blob to persist.
624
+ *
625
+ * A run over thousands of records can produce more evidence than belongs in a
626
+ * text column. Past the ceiling it stores per-record STATUS COUNTS and says so
627
+ * with `kind: "summary-only"` — a visible, different value. It never silently
628
+ * truncates, and it never fails a run whose money is already spent.
629
+ */
630
+ function storeableOutcomes(outcomes: RecordOutcome[]): {
631
+ json: string;
632
+ kind: "full" | "summary-only";
633
+ } {
634
+ const full = JSON.stringify(outcomes);
635
+ if (full.length <= MAX_OUTCOMES_CHARS) return { json: full, kind: "full" };
636
+ const summary = outcomes.map((outcome) => ({
637
+ recordId: outcome.recordId,
638
+ evidence: "summary-only",
639
+ slots: outcome.slots.map((slot) => ({
640
+ slot: slot.slot,
641
+ status: slot.status,
642
+ ...(slot.status === "error" ? { error: slot.error } : {}),
643
+ })),
644
+ ...(outcome.writes ? { writes: outcome.writes } : {}),
645
+ }));
646
+ return { json: JSON.stringify(summary), kind: "summary-only" };
647
+ }