@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,97 @@
1
+ import { defineAction } from "@agent-native/core/action";
2
+ import { accessFilter } from "@agent-native/core/sharing";
3
+ import { and, eq } from "drizzle-orm";
4
+ import { z } from "zod";
5
+
6
+ import { getDb, schema } from "../server/db/index.js";
7
+ import { CrmListError, requireCrmList } from "./_crm-list-utils.js";
8
+
9
+ export default defineAction({
10
+ description:
11
+ "Update a CRM list's name, description, ordering, default view, or archived state. `api_slug` is immutable once assigned and cannot be changed here. Archiving hides the list; it never removes its entries or their records.",
12
+ schema: z.object({
13
+ listId: z.string().trim().min(1).max(128),
14
+ name: z.string().trim().min(1).max(120).optional(),
15
+ description: z.string().trim().max(500).optional(),
16
+ position: z.number().int().min(0).max(100_000).optional(),
17
+ archived: z.boolean().optional(),
18
+ defaultViewId: z
19
+ .string()
20
+ .trim()
21
+ .min(1)
22
+ .max(128)
23
+ .nullable()
24
+ .optional()
25
+ .describe("Saved view to open this list with. Pass null to clear it."),
26
+ }),
27
+ audit: {
28
+ target: (_args, result) => {
29
+ const list = result as {
30
+ id: string;
31
+ ownerEmail: string;
32
+ orgId: string | null;
33
+ visibility: "private" | "org";
34
+ };
35
+ return {
36
+ type: "crm-list",
37
+ id: list.id,
38
+ ownerEmail: list.ownerEmail,
39
+ orgId: list.orgId,
40
+ visibility: list.visibility,
41
+ };
42
+ },
43
+ summary: (args) => `Updated CRM list ${args.listId}`,
44
+ },
45
+ run: async (args) => {
46
+ const db = getDb();
47
+ await requireCrmList(db, args.listId, "editor");
48
+
49
+ if (args.defaultViewId) {
50
+ const [view] = await db
51
+ .select({ id: schema.crmSavedViews.id })
52
+ .from(schema.crmSavedViews)
53
+ .where(
54
+ and(
55
+ eq(schema.crmSavedViews.id, args.defaultViewId),
56
+ accessFilter(schema.crmSavedViews, schema.crmSavedViewShares),
57
+ ),
58
+ )
59
+ .limit(1);
60
+ if (!view) {
61
+ throw new CrmListError(
62
+ "crm-saved-view-not-found",
63
+ `Saved view "${args.defaultViewId}" was not found or is not visible to you.`,
64
+ );
65
+ }
66
+ }
67
+
68
+ const now = new Date().toISOString();
69
+ await db
70
+ .update(schema.crmLists)
71
+ .set({
72
+ ...(args.name !== undefined ? { name: args.name } : {}),
73
+ ...(args.description !== undefined
74
+ ? { description: args.description }
75
+ : {}),
76
+ ...(args.position !== undefined ? { position: args.position } : {}),
77
+ ...(args.archived !== undefined ? { archived: args.archived } : {}),
78
+ ...(args.defaultViewId !== undefined
79
+ ? { defaultViewId: args.defaultViewId }
80
+ : {}),
81
+ updatedAt: now,
82
+ })
83
+ .where(
84
+ and(
85
+ eq(schema.crmLists.id, args.listId),
86
+ accessFilter(
87
+ schema.crmLists,
88
+ schema.crmListShares,
89
+ undefined,
90
+ "editor",
91
+ ),
92
+ ),
93
+ );
94
+
95
+ return requireCrmList(db, args.listId, "viewer");
96
+ },
97
+ });
@@ -0,0 +1,543 @@
1
+ import { defineAction, type ActionRunContext } from "@agent-native/core/action";
2
+ import { accessFilter, assertAccess } from "@agent-native/core/sharing";
3
+ import { and, eq } from "drizzle-orm";
4
+ import { z } from "zod";
5
+
6
+ import { createNativeCrmAdapter } from "../server/crm/native-adapter.js";
7
+ import { getDb, schema } from "../server/db/index.js";
8
+ import {
9
+ assertCrmStatusTransitionAllowed,
10
+ CrmLifecycleError,
11
+ loadCrmStatusLifecycle,
12
+ } from "../server/lib/lifecycle.js";
13
+ import { decideCrmWritePolicy, type CrmValue } from "../shared/crm-contract.js";
14
+ import { resolveCrmSalesDelegatedWrite } from "../shared/crm-sales-config.js";
15
+ import {
16
+ crmInitiatedBy,
17
+ crmWriteRisk,
18
+ isSafeCrmMutationFields,
19
+ MAX_CRM_FIELDS_PER_MUTATION,
20
+ scopedCrmIdempotencyKey,
21
+ toJson,
22
+ } from "./_crm-action-utils.js";
23
+
24
+ export const fieldPatchSchema = z
25
+ .record(z.string().trim().min(1).max(120), z.unknown())
26
+ .refine(
27
+ (fields) =>
28
+ Object.keys(fields).length >= 1 &&
29
+ Object.keys(fields).length <= MAX_CRM_FIELDS_PER_MUTATION,
30
+ `Provide between 1 and ${MAX_CRM_FIELDS_PER_MUTATION} fields.`,
31
+ )
32
+ .refine(
33
+ isSafeCrmMutationFields,
34
+ "CRM fields and values cannot contain media, transcripts, data URLs, base64, or oversized JSON.",
35
+ );
36
+
37
+ type CrmTransaction = Parameters<
38
+ Parameters<ReturnType<typeof getDb>["transaction"]>[0]
39
+ >[0];
40
+
41
+ function fieldColumns(value: CrmValue) {
42
+ if (typeof value === "string") {
43
+ return {
44
+ stringValue: value,
45
+ numberValue: null,
46
+ booleanValue: null,
47
+ jsonValue: null,
48
+ };
49
+ }
50
+ if (typeof value === "number") {
51
+ return {
52
+ stringValue: null,
53
+ numberValue: value,
54
+ booleanValue: null,
55
+ jsonValue: null,
56
+ };
57
+ }
58
+ if (typeof value === "boolean") {
59
+ return {
60
+ stringValue: null,
61
+ numberValue: null,
62
+ booleanValue: value,
63
+ jsonValue: null,
64
+ };
65
+ }
66
+ if (value === null) {
67
+ return {
68
+ stringValue: null,
69
+ numberValue: null,
70
+ booleanValue: null,
71
+ jsonValue: null,
72
+ };
73
+ }
74
+ return {
75
+ stringValue: null,
76
+ numberValue: null,
77
+ booleanValue: null,
78
+ jsonValue: toJson(value, 8_000),
79
+ };
80
+ }
81
+
82
+ function needsLocalApproval(args: {
83
+ target: "local" | "provider";
84
+ fields: Record<string, unknown>;
85
+ }) {
86
+ if (args.target !== "local") return false;
87
+ const risk = crmWriteRisk(Object.keys(args.fields));
88
+ return risk !== "routine" || Object.keys(args.fields).length > 1;
89
+ }
90
+
91
+ function isUniqueConstraintError(error: unknown): boolean {
92
+ const message = error instanceof Error ? error.message : "";
93
+ return /unique constraint|duplicate key|already exists/i.test(message);
94
+ }
95
+
96
+ async function updateLocalFields(input: {
97
+ tx: CrmTransaction;
98
+ record: typeof schema.crmRecords.$inferSelect;
99
+ fields: Record<string, CrmValue>;
100
+ policies: Array<typeof schema.crmFieldPolicies.$inferSelect>;
101
+ }) {
102
+ const now = new Date().toISOString();
103
+ const policyByName = new Map(
104
+ input.policies.map((policy) => [policy.fieldName, policy]),
105
+ );
106
+ for (const [fieldName, value] of Object.entries(input.fields)) {
107
+ const policy = policyByName.get(fieldName)!;
108
+ const values = {
109
+ ...fieldColumns(value),
110
+ valueType: policy.valueType,
111
+ storagePolicy: "local-authoritative" as const,
112
+ provenanceJson: toJson(
113
+ [
114
+ {
115
+ provider: "native",
116
+ connectionId: input.record.connectionId,
117
+ objectType: input.record.objectType,
118
+ remoteId: input.record.remoteId,
119
+ fieldName,
120
+ observedAt: now,
121
+ },
122
+ ],
123
+ 2_000,
124
+ ),
125
+ accessScopeKey: input.record.accessScopeKey,
126
+ accessScopeJson: input.record.accessScopeJson,
127
+ updatedAt: now,
128
+ };
129
+ const existing = await input.tx
130
+ .select({ id: schema.crmRecordFields.id })
131
+ .from(schema.crmRecordFields)
132
+ .where(
133
+ and(
134
+ eq(schema.crmRecordFields.recordId, input.record.id),
135
+ eq(schema.crmRecordFields.fieldName, fieldName),
136
+ accessFilter(schema.crmRecordFields, schema.crmRecordFieldShares),
137
+ ),
138
+ )
139
+ .limit(1);
140
+ if (existing[0]) {
141
+ await input.tx
142
+ .update(schema.crmRecordFields)
143
+ .set(values)
144
+ .where(
145
+ and(
146
+ eq(schema.crmRecordFields.id, existing[0].id),
147
+ accessFilter(
148
+ schema.crmRecordFields,
149
+ schema.crmRecordFieldShares,
150
+ undefined,
151
+ "editor",
152
+ ),
153
+ ),
154
+ );
155
+ } else {
156
+ await input.tx.insert(schema.crmRecordFields).values({
157
+ id: crypto.randomUUID(),
158
+ recordId: input.record.id,
159
+ fieldPolicyId: policy.id,
160
+ fieldName,
161
+ remoteRevision: null,
162
+ ...values,
163
+ ownerEmail: input.record.ownerEmail,
164
+ orgId: input.record.orgId,
165
+ visibility: input.record.visibility,
166
+ createdAt: now,
167
+ });
168
+ }
169
+ }
170
+
171
+ if (
172
+ Object.prototype.hasOwnProperty.call(input.fields, "desiredCadenceDays")
173
+ ) {
174
+ const cadence = input.fields.desiredCadenceDays;
175
+ if (
176
+ cadence !== null &&
177
+ (typeof cadence !== "number" ||
178
+ !Number.isInteger(cadence) ||
179
+ cadence < 1 ||
180
+ cadence > 365)
181
+ ) {
182
+ throw new Error(
183
+ "desiredCadenceDays must be null or an integer from 1 to 365.",
184
+ );
185
+ }
186
+ await input.tx
187
+ .update(schema.crmRecords)
188
+ .set({ desiredCadenceDays: cadence, updatedAt: now })
189
+ .where(
190
+ and(
191
+ eq(schema.crmRecords.id, input.record.id),
192
+ accessFilter(
193
+ schema.crmRecords,
194
+ schema.crmRecordShares,
195
+ undefined,
196
+ "editor",
197
+ ),
198
+ ),
199
+ );
200
+ }
201
+ }
202
+
203
+ export default defineAction({
204
+ description:
205
+ "Submit a revision-aware CRM record update. The initial release always saves HubSpot and Salesforce provider updates as proposals and fails closed unless an atomic expected-revision write path is proven. Local-authoritative fields may be applied after the applicable approval policy is satisfied.",
206
+ schema: z.object({
207
+ recordId: z.string().trim().min(1).max(128),
208
+ target: z.enum(["local", "provider"]).default("provider"),
209
+ fields: fieldPatchSchema,
210
+ expectedRemoteRevision: z.string().trim().min(1).max(256).optional(),
211
+ idempotencyKey: z.string().trim().min(1).max(256).optional(),
212
+ }),
213
+ needsApproval: needsLocalApproval,
214
+ audit: {
215
+ target: (_args, result) => {
216
+ const response = result as {
217
+ recordId: string;
218
+ ownerEmail: string;
219
+ orgId: string | null;
220
+ visibility: "private" | "org";
221
+ };
222
+ return {
223
+ type: "crm-record",
224
+ id: response.recordId,
225
+ ownerEmail: response.ownerEmail,
226
+ orgId: response.orgId,
227
+ visibility: response.visibility,
228
+ };
229
+ },
230
+ summary: (args) =>
231
+ `Submitted CRM ${args.target} update for record ${args.recordId}`,
232
+ recordInputs: false,
233
+ },
234
+ run: async (args, ctx?: ActionRunContext) => {
235
+ await assertAccess("crm-record", args.recordId, "editor");
236
+ const db = getDb();
237
+ const [record] = await db
238
+ .select()
239
+ .from(schema.crmRecords)
240
+ .where(
241
+ and(
242
+ eq(schema.crmRecords.id, args.recordId),
243
+ accessFilter(
244
+ schema.crmRecords,
245
+ schema.crmRecordShares,
246
+ undefined,
247
+ "editor",
248
+ ),
249
+ ),
250
+ )
251
+ .limit(1);
252
+ if (!record || record.tombstone)
253
+ throw new Error("CRM record is unavailable for updates.");
254
+
255
+ const fields = args.fields as Record<string, CrmValue>;
256
+ const fieldNames = Object.keys(fields);
257
+ const policies = await db
258
+ .select()
259
+ .from(schema.crmFieldPolicies)
260
+ .where(
261
+ and(
262
+ eq(schema.crmFieldPolicies.connectionId, record.connectionId),
263
+ eq(schema.crmFieldPolicies.objectType, record.objectType),
264
+ accessFilter(schema.crmFieldPolicies, schema.crmFieldPolicyShares),
265
+ ),
266
+ );
267
+ const policyByName = new Map(
268
+ policies.map((policy) => [policy.fieldName, policy]),
269
+ );
270
+ const missing = fieldNames.filter(
271
+ (fieldName) => !policyByName.has(fieldName),
272
+ );
273
+ const blocked = fieldNames.filter((fieldName) => {
274
+ const policy = policyByName.get(fieldName);
275
+ return (
276
+ !policy || !policy.updateable || policy.storagePolicy === "redacted"
277
+ );
278
+ });
279
+ if (missing.length || blocked.length) {
280
+ const error = new Error(
281
+ `Only discovered, updateable CRM fields can be changed. Unsupported: ${[...new Set([...missing, ...blocked])].join(", ")}`,
282
+ ) as Error & { statusCode?: number };
283
+ error.statusCode = 422;
284
+ throw error;
285
+ }
286
+
287
+ const wrongAuthority = fieldNames.filter((fieldName) => {
288
+ const storagePolicy = policyByName.get(fieldName)?.storagePolicy;
289
+ return args.target === "local"
290
+ ? storagePolicy !== "local-authoritative"
291
+ : storagePolicy === "derived-local" ||
292
+ storagePolicy === "local-authoritative";
293
+ });
294
+ if (wrongAuthority.length) {
295
+ throw new Error(
296
+ `${args.target === "local" ? "Local" : "Provider"} authority does not own: ${wrongAuthority.join(", ")}`,
297
+ );
298
+ }
299
+
300
+ // A status value is a lifecycle transition, not just another local value:
301
+ // the enterable set is the attribute's own options, so a move into a
302
+ // retired or undeclared stage is refused with the reason a person can act
303
+ // on. Only local writes are gated — a provider-target status change is a
304
+ // proposal, which is the working flow, not a blocked transition.
305
+ if (args.target === "local") {
306
+ for (const fieldName of fieldNames) {
307
+ const policy = policyByName.get(fieldName)!;
308
+ if (policy.attributeType !== "status") continue;
309
+ const value = fields[fieldName];
310
+ // Leaving a status is always allowed, including a stage that is now
311
+ // retired; only entering one is governed.
312
+ if (value === null) continue;
313
+ if (typeof value !== "string") {
314
+ throw new CrmLifecycleError(
315
+ "crm-status-value-type",
316
+ `"${fieldName}" is a status attribute; its value must be one of its declared options.`,
317
+ );
318
+ }
319
+ await assertCrmStatusTransitionAllowed({
320
+ db,
321
+ lifecycle: await loadCrmStatusLifecycle(db, policy.id),
322
+ target: { recordId: record.id },
323
+ to: value,
324
+ });
325
+ }
326
+ }
327
+
328
+ const risk = crmWriteRisk(fieldNames);
329
+ const initiatedBy = crmInitiatedBy(ctx);
330
+ const writePolicy = {
331
+ initiatedBy,
332
+ target: args.target,
333
+ reversibility: "compensatable",
334
+ scope: fieldNames.length === 1 ? "single-field" : "single-record",
335
+ risk,
336
+ } as const;
337
+ const delegation = resolveCrmSalesDelegatedWrite({
338
+ context: ctx,
339
+ operation: "update",
340
+ policy: {
341
+ ...writePolicy,
342
+ delegatedAuthority: false,
343
+ storedAutomationPolicy: false,
344
+ },
345
+ });
346
+ const decision = decideCrmWritePolicy({ ...writePolicy, ...delegation });
347
+ if (decision === "deny")
348
+ throw new Error(
349
+ "This CRM update is not authorized by the current write policy.",
350
+ );
351
+ if (
352
+ initiatedBy === "automation" &&
353
+ args.target === "local" &&
354
+ decision !== "execute"
355
+ ) {
356
+ throw new Error(
357
+ "This CRM automation may only execute an explicitly delegated routine local update.",
358
+ );
359
+ }
360
+
361
+ const ownership = {
362
+ ownerEmail: record.ownerEmail,
363
+ orgId: record.orgId,
364
+ visibility: record.visibility,
365
+ };
366
+ const patchJson = toJson({ fields }, 12_000);
367
+ const expectedRemoteRevision =
368
+ args.target === "provider"
369
+ ? (args.expectedRemoteRevision ?? record.remoteRevision)
370
+ : (args.expectedRemoteRevision ?? null);
371
+ if (args.target === "provider" && !expectedRemoteRevision) {
372
+ throw new Error(
373
+ "CRM provider updates require a current remote revision. Refresh the record before proposing a change.",
374
+ );
375
+ }
376
+ const idempotencyKey = await scopedCrmIdempotencyKey({
377
+ ...ownership,
378
+ recordId: record.id,
379
+ key: args.idempotencyKey ?? crypto.randomUUID(),
380
+ });
381
+ const findExisting = async () => {
382
+ const [existing] = await db
383
+ .select()
384
+ .from(schema.crmMutations)
385
+ .where(
386
+ and(
387
+ eq(schema.crmMutations.idempotencyKey, idempotencyKey),
388
+ accessFilter(schema.crmMutations, schema.crmMutationShares),
389
+ ),
390
+ )
391
+ .limit(1);
392
+ return existing;
393
+ };
394
+ const replay = (existing: typeof schema.crmMutations.$inferSelect) => {
395
+ if (
396
+ existing.recordId !== record.id ||
397
+ existing.target !== args.target ||
398
+ existing.patchJson !== patchJson ||
399
+ existing.expectedRemoteRevision !== expectedRemoteRevision
400
+ ) {
401
+ throw new Error(
402
+ "CRM idempotency key was already used for a different mutation.",
403
+ );
404
+ }
405
+ return {
406
+ mutationId: existing.id,
407
+ recordId: record.id,
408
+ status: existing.status,
409
+ decision: existing.policyDecision,
410
+ replayed: true as const,
411
+ ownerEmail: record.ownerEmail,
412
+ orgId: record.orgId,
413
+ visibility: record.visibility,
414
+ };
415
+ };
416
+ const existing = await findExisting();
417
+ if (existing) return replay(existing);
418
+
419
+ const now = new Date().toISOString();
420
+ const mutationId = crypto.randomUUID();
421
+ if (args.target === "local" && record.provider === "native") {
422
+ if (!args.expectedRemoteRevision) {
423
+ throw new Error(
424
+ "Native SQL CRM updates require the current record revision. Refresh the record and retry.",
425
+ );
426
+ }
427
+ const adapter = await createNativeCrmAdapter({
428
+ connectionId: record.connectionId,
429
+ initiatedBy,
430
+ });
431
+ const result = await adapter.applyMutation({
432
+ operation: "update",
433
+ record: {
434
+ connectionId: record.connectionId,
435
+ provider: "native",
436
+ objectType: record.objectType,
437
+ kind: record.kind,
438
+ remoteId: record.remoteId,
439
+ localId: record.id,
440
+ },
441
+ fields,
442
+ expectedRemoteRevision: args.expectedRemoteRevision,
443
+ idempotencyKey,
444
+ });
445
+ if (result.status !== "applied") {
446
+ throw new Error(
447
+ result.message ?? `Native SQL CRM update ${result.status}.`,
448
+ );
449
+ }
450
+ const applied = await findExisting();
451
+ if (!applied) {
452
+ throw new Error(
453
+ "Native SQL CRM update succeeded but its audit mutation could not be verified.",
454
+ );
455
+ }
456
+ return {
457
+ mutationId: applied.id,
458
+ recordId: record.id,
459
+ status: "applied" as const,
460
+ revision: result.remoteRevision,
461
+ ownerEmail: record.ownerEmail,
462
+ orgId: record.orgId,
463
+ visibility: record.visibility,
464
+ };
465
+ }
466
+ if (args.target === "local") {
467
+ try {
468
+ await db.transaction(async (tx) => {
469
+ await updateLocalFields({ tx, record, fields, policies });
470
+ await tx.insert(schema.crmMutations).values({
471
+ id: mutationId,
472
+ recordId: record.id,
473
+ connectionId: record.connectionId,
474
+ operation: "update",
475
+ initiatedBy,
476
+ target: "local",
477
+ policyDecision: decision,
478
+ risk,
479
+ status: "applied",
480
+ patchJson,
481
+ beforeJson: "{}",
482
+ afterJson: patchJson,
483
+ idempotencyKey,
484
+ expectedRemoteRevision,
485
+ appliedAt: now,
486
+ ...ownership,
487
+ createdAt: now,
488
+ updatedAt: now,
489
+ });
490
+ });
491
+ } catch (error) {
492
+ if (!isUniqueConstraintError(error)) throw error;
493
+ const raced = await findExisting();
494
+ if (raced) return replay(raced);
495
+ throw error;
496
+ }
497
+ return {
498
+ mutationId,
499
+ recordId: record.id,
500
+ status: "applied" as const,
501
+ ownerEmail: record.ownerEmail,
502
+ orgId: record.orgId,
503
+ visibility: record.visibility,
504
+ };
505
+ }
506
+
507
+ try {
508
+ await db.insert(schema.crmMutations).values({
509
+ id: mutationId,
510
+ recordId: record.id,
511
+ connectionId: record.connectionId,
512
+ operation: "update",
513
+ initiatedBy,
514
+ target: "provider",
515
+ policyDecision: decision,
516
+ risk,
517
+ status: "pending",
518
+ patchJson,
519
+ beforeJson: toJson({ remoteRevision: record.remoteRevision }, 1_000),
520
+ afterJson: "{}",
521
+ idempotencyKey,
522
+ expectedRemoteRevision,
523
+ ...ownership,
524
+ createdAt: now,
525
+ updatedAt: now,
526
+ });
527
+ } catch (error) {
528
+ if (!isUniqueConstraintError(error)) throw error;
529
+ const raced = await findExisting();
530
+ if (raced) return replay(raced);
531
+ throw error;
532
+ }
533
+ return {
534
+ mutationId,
535
+ recordId: record.id,
536
+ status: "pending" as const,
537
+ decision,
538
+ ownerEmail: record.ownerEmail,
539
+ orgId: record.orgId,
540
+ visibility: record.visibility,
541
+ };
542
+ },
543
+ });