@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,79 @@
1
+ import type { ActionRunContext } from "@agent-native/core/action";
2
+ import { accessFilter, assertAccess } from "@agent-native/core/sharing";
3
+ import { and, eq, inArray } from "drizzle-orm";
4
+
5
+ import { getDb, schema } from "../server/db/index.js";
6
+ import type { CallEvidenceExcerpt } from "../server/lib/intelligence/evidence.js";
7
+ import {
8
+ requireCrmScope,
9
+ scopedCrmIdempotencyKey,
10
+ } from "./_crm-action-utils.js";
11
+
12
+ export async function loadCrmSignalEvidence(args: {
13
+ recordId: string;
14
+ evidenceIds?: string[];
15
+ role?: "viewer" | "editor";
16
+ }) {
17
+ await assertAccess("crm-record", args.recordId, args.role ?? "viewer");
18
+ const db = getDb();
19
+ const predicates = [
20
+ eq(schema.crmCallEvidence.recordId, args.recordId),
21
+ accessFilter(schema.crmCallEvidence, schema.crmCallEvidenceShares),
22
+ ];
23
+ if (args.evidenceIds?.length) {
24
+ predicates.push(inArray(schema.crmCallEvidence.id, args.evidenceIds));
25
+ }
26
+ const rows = await db
27
+ .select()
28
+ .from(schema.crmCallEvidence)
29
+ .where(and(...predicates))
30
+ .limit(20);
31
+ if (
32
+ args.evidenceIds?.length &&
33
+ rows.length !== new Set(args.evidenceIds).size
34
+ ) {
35
+ throw new Error("One or more call-evidence references are unavailable.");
36
+ }
37
+ return rows;
38
+ }
39
+
40
+ export function evidenceExcerpts(
41
+ rows: Awaited<ReturnType<typeof loadCrmSignalEvidence>>,
42
+ ): CallEvidenceExcerpt[] {
43
+ return rows.flatMap((row) => {
44
+ const quote = row.quote.trim() || row.summary.trim();
45
+ if (!quote) return [];
46
+ return [
47
+ {
48
+ evidenceRef: row.id,
49
+ quote,
50
+ ...(row.speaker ? { speaker: row.speaker } : {}),
51
+ startSeconds: row.startSeconds ?? 0,
52
+ ...(row.endSeconds !== null ? { endSeconds: row.endSeconds } : {}),
53
+ },
54
+ ];
55
+ });
56
+ }
57
+
58
+ export async function crmSignalIdempotencyKey(
59
+ ctx: ActionRunContext | undefined,
60
+ recordId: string,
61
+ key: string,
62
+ ) {
63
+ const scope = requireCrmScope(ctx);
64
+ return {
65
+ scope,
66
+ key: await scopedCrmIdempotencyKey({
67
+ ownerEmail: scope.ownerEmail,
68
+ orgId: scope.orgId,
69
+ recordId,
70
+ key: `signal:${key}`,
71
+ }),
72
+ };
73
+ }
74
+
75
+ export function signalTupleError(message: string): never {
76
+ const error = new Error(message) as Error & { statusCode?: number };
77
+ error.statusCode = 404;
78
+ throw error;
79
+ }
@@ -0,0 +1,164 @@
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 {
8
+ isSafeCrmMutationFields,
9
+ MAX_CRM_FIELDS_PER_MUTATION,
10
+ requireCrmScope,
11
+ } from "./_crm-action-utils.js";
12
+ import {
13
+ CrmListError,
14
+ crmActorFrom,
15
+ indexAttributes,
16
+ loadCrmListAttributes,
17
+ requireCrmList,
18
+ writeCrmListEntryValues,
19
+ } from "./_crm-list-utils.js";
20
+
21
+ export const listEntryValuesSchema = z
22
+ .record(z.string().trim().min(1).max(120), z.unknown())
23
+ .refine(
24
+ (values) => Object.keys(values).length <= MAX_CRM_FIELDS_PER_MUTATION,
25
+ `Provide at most ${MAX_CRM_FIELDS_PER_MUTATION} entry attributes.`,
26
+ )
27
+ .refine(
28
+ isSafeCrmMutationFields,
29
+ "CRM entry values cannot contain media, transcripts, data URLs, base64, or oversized JSON.",
30
+ );
31
+
32
+ export default defineAction({
33
+ description:
34
+ "Add a record to a CRM list as a new entry, with optional starting values for the list's own entry attributes. A record may hold more than one entry in the same list — two open deals for one company are two entries — so this never de-duplicates; it returns existingEntryIds for the caller to decide. The record's objectType must match the list's parentObjectType. The entry is local-authoritative even when the record mirrors HubSpot or Salesforce.",
35
+ schema: z.object({
36
+ listId: z.string().trim().min(1).max(128),
37
+ recordId: z.string().trim().min(1).max(128),
38
+ values: listEntryValuesSchema.optional(),
39
+ position: z.number().int().min(0).max(100_000).optional(),
40
+ }),
41
+ audit: {
42
+ target: (_args, result) => {
43
+ const entry = result as {
44
+ entryId: string;
45
+ ownerEmail: string;
46
+ orgId: string | null;
47
+ visibility: "private" | "org";
48
+ };
49
+ return {
50
+ type: "crm-list-entry",
51
+ id: entry.entryId,
52
+ ownerEmail: entry.ownerEmail,
53
+ orgId: entry.orgId,
54
+ visibility: entry.visibility,
55
+ };
56
+ },
57
+ summary: (args) =>
58
+ `Added CRM record ${args.recordId} to list ${args.listId}`,
59
+ recordInputs: false,
60
+ },
61
+ run: async (args, ctx?: ActionRunContext) => {
62
+ const ownership = requireCrmScope(ctx);
63
+ const db = getDb();
64
+ const list = await requireCrmList(db, args.listId, "editor");
65
+
66
+ const [record] = await db
67
+ .select({
68
+ id: schema.crmRecords.id,
69
+ objectType: schema.crmRecords.objectType,
70
+ displayName: schema.crmRecords.displayName,
71
+ })
72
+ .from(schema.crmRecords)
73
+ .where(
74
+ and(
75
+ eq(schema.crmRecords.id, args.recordId),
76
+ eq(schema.crmRecords.tombstone, false),
77
+ accessFilter(schema.crmRecords, schema.crmRecordShares),
78
+ ),
79
+ )
80
+ .limit(1);
81
+ if (!record) {
82
+ throw new CrmListError(
83
+ "crm-record-not-found",
84
+ `CRM record "${args.recordId}" was not found or is not visible to you.`,
85
+ );
86
+ }
87
+ if (record.objectType !== list.parentObjectType) {
88
+ throw new CrmListError(
89
+ "crm-list-object-type-mismatch",
90
+ `List "${list.name}" holds ${list.parentObjectType}; record ${record.id} is ${record.objectType}.`,
91
+ );
92
+ }
93
+
94
+ const existing = await db
95
+ .select({ id: schema.crmListEntries.id })
96
+ .from(schema.crmListEntries)
97
+ .where(
98
+ and(
99
+ eq(schema.crmListEntries.listId, list.id),
100
+ eq(schema.crmListEntries.recordId, record.id),
101
+ accessFilter(schema.crmListEntries, schema.crmListEntryShares),
102
+ ),
103
+ )
104
+ .limit(50);
105
+
106
+ const attributes = indexAttributes(
107
+ await loadCrmListAttributes(db, list.id),
108
+ );
109
+ const [last] = await db
110
+ .select({ position: schema.crmListEntries.position })
111
+ .from(schema.crmListEntries)
112
+ .where(
113
+ and(
114
+ eq(schema.crmListEntries.listId, list.id),
115
+ accessFilter(schema.crmListEntries, schema.crmListEntryShares),
116
+ ),
117
+ )
118
+ .orderBy(desc(schema.crmListEntries.position))
119
+ .limit(1);
120
+
121
+ const entryId = crypto.randomUUID();
122
+ const actor = crmActorFrom(ctx);
123
+ const now = new Date().toISOString();
124
+
125
+ // One transaction so a rejected value (an unknown status option, say) can
126
+ // never leave a half-populated entry behind.
127
+ const writes = await db.transaction(async (tx) => {
128
+ await tx.insert(schema.crmListEntries).values({
129
+ id: entryId,
130
+ listId: list.id,
131
+ recordId: record.id,
132
+ position: args.position ?? (last ? last.position + 1 : 0),
133
+ createdByActorType: actor.type,
134
+ createdByActorId: actor.id,
135
+ ...ownership,
136
+ createdAt: now,
137
+ updatedAt: now,
138
+ });
139
+ return writeCrmListEntryValues({
140
+ db: tx,
141
+ recordId: record.id,
142
+ entryId,
143
+ attributes,
144
+ values: args.values ?? {},
145
+ actor,
146
+ ownership,
147
+ now,
148
+ });
149
+ });
150
+
151
+ return {
152
+ entryId,
153
+ listId: list.id,
154
+ recordId: record.id,
155
+ /**
156
+ * Entries this record already had in this list. Non-empty is normal, not
157
+ * an error — the caller decides whether a second entry was intended.
158
+ */
159
+ existingEntryIds: existing.map((entry) => entry.id),
160
+ values: writes,
161
+ ...ownership,
162
+ };
163
+ },
164
+ });
@@ -0,0 +1,291 @@
1
+ import { defineAction, type ActionRunContext } from "@agent-native/core/action";
2
+ import { accessFilter, assertAccess } from "@agent-native/core/sharing";
3
+ import { and, eq, inArray, isNull } from "drizzle-orm";
4
+ import { z } from "zod";
5
+
6
+ import { isConnectedCrmProvider } from "../server/crm/adapter.js";
7
+ import {
8
+ resolveProviderRecordLink,
9
+ type ProviderRecordLink,
10
+ } from "../server/crm/provider-record-link.js";
11
+ import { getDb, schema } from "../server/db/index.js";
12
+ import {
13
+ isSafeCrmMutationFields,
14
+ parseJsonRecord,
15
+ requireCrmScope,
16
+ } from "./_crm-action-utils.js";
17
+
18
+ const PROVIDER_LABELS = {
19
+ hubspot: "HubSpot",
20
+ salesforce: "Salesforce",
21
+ } as const;
22
+
23
+ /**
24
+ * A prepared field change. `beforeKnown` separates "the mirror holds no value
25
+ * for this field" from "the mirrored value is empty" — collapsing them would
26
+ * show a confident `— → Renewal` diff for a field we simply do not mirror.
27
+ */
28
+ interface PreparedField {
29
+ name: string;
30
+ beforeKnown: boolean;
31
+ before: string | number | boolean | null;
32
+ after: unknown;
33
+ }
34
+
35
+ function updateAffectedRows(result: unknown): number {
36
+ if (!result || typeof result !== "object") return 0;
37
+ const value = result as {
38
+ rowsAffected?: unknown;
39
+ rowCount?: unknown;
40
+ count?: unknown;
41
+ changes?: unknown;
42
+ meta?: { changes?: unknown };
43
+ };
44
+ for (const count of [
45
+ value.rowsAffected,
46
+ value.rowCount,
47
+ value.count,
48
+ value.changes,
49
+ value.meta?.changes,
50
+ ]) {
51
+ if (typeof count === "number") return count;
52
+ }
53
+ return 0;
54
+ }
55
+
56
+ function currentMirroredValue(row: {
57
+ stringValue: string | null;
58
+ numberValue: number | null;
59
+ booleanValue: boolean | null;
60
+ jsonValue: string | null;
61
+ }): string | number | boolean | null {
62
+ if (row.stringValue !== null) return row.stringValue;
63
+ if (row.numberValue !== null) return row.numberValue;
64
+ if (row.booleanValue !== null) return row.booleanValue;
65
+ return row.jsonValue;
66
+ }
67
+
68
+ export default defineAction({
69
+ description:
70
+ "Prepare one pending HubSpot or Salesforce record change for completion upstream. Returns the exact before/after diff and a deep link to the record, and records the handoff in the proposal ledger. It never writes to the provider and never reports an upstream change as applied.",
71
+ schema: z.object({
72
+ proposalId: z.string().trim().min(1).max(128),
73
+ }),
74
+ needsApproval: true,
75
+ audit: {
76
+ target: (_args, result) => {
77
+ const response = result as {
78
+ recordId: string;
79
+ ownerEmail: string;
80
+ orgId: string | null;
81
+ visibility: "private" | "org";
82
+ };
83
+ return {
84
+ type: "crm-record",
85
+ id: response.recordId,
86
+ ownerEmail: response.ownerEmail,
87
+ orgId: response.orgId,
88
+ visibility: response.visibility,
89
+ };
90
+ },
91
+ summary: (args) =>
92
+ `Prepared CRM proposal ${args.proposalId} for upstream completion`,
93
+ recordInputs: false,
94
+ },
95
+ run: async (args, ctx?: ActionRunContext) => {
96
+ await assertAccess("crm-mutation", args.proposalId, "editor");
97
+ const db = getDb();
98
+ const [proposal] = await db
99
+ .select()
100
+ .from(schema.crmMutations)
101
+ .where(
102
+ and(
103
+ eq(schema.crmMutations.id, args.proposalId),
104
+ accessFilter(
105
+ schema.crmMutations,
106
+ schema.crmMutationShares,
107
+ undefined,
108
+ "editor",
109
+ ),
110
+ ),
111
+ )
112
+ .limit(1);
113
+ if (!proposal) throw new Error("CRM proposal was not found.");
114
+ if (proposal.target !== "provider" || proposal.operation !== "update") {
115
+ throw new Error(
116
+ "Only pending provider update proposals can be prepared in this phase.",
117
+ );
118
+ }
119
+ if (proposal.status !== "pending") {
120
+ throw new Error(
121
+ `CRM proposal is ${proposal.status} and cannot be prepared again.`,
122
+ );
123
+ }
124
+ if (!proposal.recordId || !proposal.connectionId) {
125
+ throw new Error(
126
+ "CRM proposal is missing its record or connection reference.",
127
+ );
128
+ }
129
+ await assertAccess("crm-record", proposal.recordId, "editor");
130
+ await assertAccess("crm-connection", proposal.connectionId, "editor");
131
+ const [[record], [connection]] = await Promise.all([
132
+ db
133
+ .select()
134
+ .from(schema.crmRecords)
135
+ .where(
136
+ and(
137
+ eq(schema.crmRecords.id, proposal.recordId),
138
+ accessFilter(
139
+ schema.crmRecords,
140
+ schema.crmRecordShares,
141
+ undefined,
142
+ "editor",
143
+ ),
144
+ ),
145
+ )
146
+ .limit(1),
147
+ db
148
+ .select()
149
+ .from(schema.crmConnections)
150
+ .where(
151
+ and(
152
+ eq(schema.crmConnections.id, proposal.connectionId),
153
+ accessFilter(
154
+ schema.crmConnections,
155
+ schema.crmConnectionShares,
156
+ undefined,
157
+ "editor",
158
+ ),
159
+ ),
160
+ )
161
+ .limit(1),
162
+ ]);
163
+ if (!record || record.tombstone || !connection) {
164
+ throw new Error(
165
+ "CRM proposal no longer has an available record and connection.",
166
+ );
167
+ }
168
+ if (!isConnectedCrmProvider(connection.provider)) {
169
+ throw new Error(
170
+ "Only HubSpot and Salesforce provider proposals are enabled in this release.",
171
+ );
172
+ }
173
+ if (!connection.workspaceConnectionId) {
174
+ throw new Error(
175
+ "CRM connection is missing its workspace connection reference.",
176
+ );
177
+ }
178
+ // Without a revision anchor the mirrored "before" column cannot be trusted
179
+ // to describe the record the user is about to edit by hand.
180
+ if (!proposal.expectedRemoteRevision) {
181
+ throw new Error(
182
+ "CRM proposal has no remote revision and must be recreated from a refreshed record.",
183
+ );
184
+ }
185
+ const patch = parseJsonRecord(proposal.patchJson);
186
+ const fields = patch.fields;
187
+ if (!fields || typeof fields !== "object" || Array.isArray(fields)) {
188
+ throw new Error("CRM proposal has an invalid field patch.");
189
+ }
190
+ const fieldPatch = fields as Record<string, unknown>;
191
+ if (!isSafeCrmMutationFields(fieldPatch)) {
192
+ throw new Error("CRM proposal contains an unsafe field patch.");
193
+ }
194
+ const fieldNames = Object.keys(fieldPatch);
195
+
196
+ const currentRows = fieldNames.length
197
+ ? await db
198
+ .select({
199
+ fieldName: schema.crmRecordFields.fieldName,
200
+ stringValue: schema.crmRecordFields.stringValue,
201
+ numberValue: schema.crmRecordFields.numberValue,
202
+ booleanValue: schema.crmRecordFields.booleanValue,
203
+ jsonValue: schema.crmRecordFields.jsonValue,
204
+ })
205
+ .from(schema.crmRecordFields)
206
+ .where(
207
+ and(
208
+ eq(schema.crmRecordFields.recordId, record.id),
209
+ isNull(schema.crmRecordFields.entryId),
210
+ isNull(schema.crmRecordFields.activeUntil),
211
+ inArray(schema.crmRecordFields.fieldName, fieldNames),
212
+ accessFilter(schema.crmRecordFields, schema.crmRecordFieldShares),
213
+ ),
214
+ )
215
+ : [];
216
+ const currentByFieldName = new Map(
217
+ currentRows.map((row) => [row.fieldName, row]),
218
+ );
219
+ const preparedFields: PreparedField[] = fieldNames.map((name) => {
220
+ const current = currentByFieldName.get(name);
221
+ return {
222
+ name,
223
+ beforeKnown: Boolean(current),
224
+ before: current ? currentMirroredValue(current) : null,
225
+ after: fieldPatch[name],
226
+ };
227
+ });
228
+
229
+ const link: ProviderRecordLink = await resolveProviderRecordLink({
230
+ provider: connection.provider,
231
+ objectType: record.objectType,
232
+ remoteId: record.remoteId,
233
+ accountId: connection.accountId,
234
+ workspaceConnectionId: connection.workspaceConnectionId,
235
+ });
236
+
237
+ const scope = requireCrmScope(ctx);
238
+ const now = new Date().toISOString();
239
+ // `approved` with `appliedAt` still null is the ledger's "prepared and
240
+ // handed off, never written upstream" state. `error` stays null so a
241
+ // working handoff is never mistaken for a failure.
242
+ const claim = await db
243
+ .update(schema.crmMutations)
244
+ .set({
245
+ status: "approved",
246
+ approvedBy: scope.ownerEmail,
247
+ approvedAt: now,
248
+ error: null,
249
+ updatedAt: now,
250
+ })
251
+ .where(
252
+ and(
253
+ eq(schema.crmMutations.id, proposal.id),
254
+ eq(schema.crmMutations.status, "pending"),
255
+ accessFilter(
256
+ schema.crmMutations,
257
+ schema.crmMutationShares,
258
+ undefined,
259
+ "editor",
260
+ ),
261
+ ),
262
+ );
263
+ if (updateAffectedRows(claim) !== 1) {
264
+ throw new Error(
265
+ "CRM proposal was already claimed by another preparation attempt.",
266
+ );
267
+ }
268
+
269
+ const providerLabel = PROVIDER_LABELS[connection.provider];
270
+ return {
271
+ proposalId: proposal.id,
272
+ recordId: record.id,
273
+ recordName: record.displayName,
274
+ status: "approved" as const,
275
+ /** This release never completes a provider write. Always false. */
276
+ upstreamApplied: false,
277
+ provider: connection.provider,
278
+ providerLabel,
279
+ preparedAt: now,
280
+ fields: preparedFields,
281
+ recordUrl: link.available ? link.url : null,
282
+ recordUrlUnavailableReason: link.available ? null : link.reason,
283
+ guidance: link.available
284
+ ? `Reviewed and ready. Open the record in ${providerLabel} and make this change there — CRM has not written it upstream.`
285
+ : `Reviewed and ready. CRM could not build a ${providerLabel} record link (${link.reason}); find the record in ${providerLabel} and make this change there. CRM has not written it upstream.`,
286
+ ownerEmail: record.ownerEmail,
287
+ orgId: record.orgId,
288
+ visibility: record.visibility,
289
+ };
290
+ },
291
+ });
@@ -0,0 +1,69 @@
1
+ import { defineAction } from "@agent-native/core/action";
2
+ import { accessFilter } from "@agent-native/core/sharing";
3
+ import { and, count, eq, isNull } from "drizzle-orm";
4
+ import { z } from "zod";
5
+
6
+ import { getDb, schema } from "../server/db/index.js";
7
+ import {
8
+ loadAttributeOptions,
9
+ requireEditableAttribute,
10
+ toAttributeDefinition,
11
+ } from "./_crm-attribute-utils.js";
12
+
13
+ export default defineAction({
14
+ description:
15
+ "Archive one CRM attribute. This is a soft archive: the attribute stops appearing in the grid and pickers, and every stored value and history row it owns is retained untouched. Attributes are never deleted. Use update-crm-attribute with archived=false to restore one.",
16
+ schema: z.object({
17
+ attributeId: z.string().trim().min(1).max(128),
18
+ }),
19
+ audit: {
20
+ target: (args) => ({ type: "crm-field-policy", id: args.attributeId }),
21
+ summary: (args) => `Archived CRM attribute ${args.attributeId}`,
22
+ },
23
+ run: async (args) => {
24
+ const current = await requireEditableAttribute(args.attributeId);
25
+ const db = getDb();
26
+
27
+ if (!current.archived) {
28
+ await db
29
+ .update(schema.crmFieldPolicies)
30
+ .set({ archived: true, updatedAt: new Date().toISOString() })
31
+ .where(eq(schema.crmFieldPolicies.id, args.attributeId));
32
+ }
33
+
34
+ // Reported so a caller can see the archive kept data rather than removed it.
35
+ const [values] = await db
36
+ .select({ total: count() })
37
+ .from(schema.crmRecordFields)
38
+ .where(
39
+ and(
40
+ eq(schema.crmRecordFields.attributeId, args.attributeId),
41
+ isNull(schema.crmRecordFields.activeUntil),
42
+ accessFilter(schema.crmRecordFields, schema.crmRecordFieldShares),
43
+ ),
44
+ );
45
+
46
+ if (!values) {
47
+ throw new Error(
48
+ "Retained CRM attribute values could not be counted; the archive was not confirmed.",
49
+ );
50
+ }
51
+
52
+ const [row] = await db
53
+ .select()
54
+ .from(schema.crmFieldPolicies)
55
+ .where(eq(schema.crmFieldPolicies.id, args.attributeId))
56
+ .limit(1);
57
+ if (!row) {
58
+ throw new Error("CRM attribute could not be verified after archiving.");
59
+ }
60
+ const options = await loadAttributeOptions({
61
+ attributeIds: [row.id],
62
+ includeArchived: true,
63
+ });
64
+ return {
65
+ ...toAttributeDefinition(row, options.get(row.id) ?? []),
66
+ retainedValueCount: values.total,
67
+ };
68
+ },
69
+ });