@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,5 @@
1
+ import { createH3SSRHandler } from "@agent-native/core/server/ssr-handler";
2
+
3
+ export default createH3SSRHandler(
4
+ () => import("virtual:react-router/server-build"),
5
+ );
@@ -0,0 +1,469 @@
1
+ /**
2
+ * The single CRM attribute-type registry.
3
+ *
4
+ * Every place that needs to know "which column does this type live in", "can it
5
+ * be multi-valued", "does it take managed options", or "which composite
6
+ * sub-columns does it populate" reads it from here. Adding a type means adding
7
+ * one entry to `ATTRIBUTE_TYPE_SPECS` — not another `switch` in an action.
8
+ */
9
+
10
+ export const CRM_ATTRIBUTE_TYPES = [
11
+ "text",
12
+ "number",
13
+ "checkbox",
14
+ "currency",
15
+ "date",
16
+ "timestamp",
17
+ "rating",
18
+ "status",
19
+ "select",
20
+ "record-reference",
21
+ "actor-reference",
22
+ "location",
23
+ "domain",
24
+ "email-address",
25
+ "phone-number",
26
+ "interaction",
27
+ "personal-name",
28
+ ] as const;
29
+
30
+ export type CrmAttributeType = (typeof CRM_ATTRIBUTE_TYPES)[number];
31
+
32
+ /** Which `crm_record_fields` value column a single (non-multi) value lands in. */
33
+ export type CrmAttributeStorageColumn =
34
+ | "stringValue"
35
+ | "numberValue"
36
+ | "booleanValue"
37
+ | "jsonValue";
38
+
39
+ /** Sparse, indexable composite columns on `crm_record_fields`. */
40
+ export type CrmRecordFieldSubColumn =
41
+ | "emailLocal"
42
+ | "emailDomain"
43
+ | "emailRootDomain"
44
+ | "phoneE164"
45
+ | "phoneCountry"
46
+ | "domainRoot"
47
+ | "nameFirst"
48
+ | "nameLast";
49
+
50
+ export const CRM_RECORD_FIELD_SUB_COLUMNS = [
51
+ "emailLocal",
52
+ "emailDomain",
53
+ "emailRootDomain",
54
+ "phoneE164",
55
+ "phoneCountry",
56
+ "domainRoot",
57
+ "nameFirst",
58
+ "nameLast",
59
+ ] as const satisfies readonly CrmRecordFieldSubColumn[];
60
+
61
+ export interface CrmAttributeTypeSpec {
62
+ /** Column a single value is stored in. Multi values always use `jsonValue`. */
63
+ storageColumn: CrmAttributeStorageColumn;
64
+ supportsMulti: boolean;
65
+ /** Values must match a `crm_attribute_options` row; unknown values are a 422. */
66
+ usesOptions: boolean;
67
+ /** Created by the system only — rejected in create-attribute input. */
68
+ systemOnly: boolean;
69
+ /** Composite sub-columns this type derives from its value. */
70
+ subFields: readonly CrmRecordFieldSubColumn[];
71
+ }
72
+
73
+ export const ATTRIBUTE_TYPE_SPECS: Record<
74
+ CrmAttributeType,
75
+ CrmAttributeTypeSpec
76
+ > = {
77
+ text: {
78
+ storageColumn: "stringValue",
79
+ supportsMulti: false,
80
+ usesOptions: false,
81
+ systemOnly: false,
82
+ subFields: [],
83
+ },
84
+ number: {
85
+ storageColumn: "numberValue",
86
+ supportsMulti: false,
87
+ usesOptions: false,
88
+ systemOnly: false,
89
+ subFields: [],
90
+ },
91
+ checkbox: {
92
+ storageColumn: "booleanValue",
93
+ supportsMulti: false,
94
+ usesOptions: false,
95
+ systemOnly: false,
96
+ subFields: [],
97
+ },
98
+ currency: {
99
+ storageColumn: "numberValue",
100
+ supportsMulti: false,
101
+ usesOptions: false,
102
+ systemOnly: false,
103
+ subFields: [],
104
+ },
105
+ date: {
106
+ storageColumn: "stringValue",
107
+ supportsMulti: false,
108
+ usesOptions: false,
109
+ systemOnly: false,
110
+ subFields: [],
111
+ },
112
+ timestamp: {
113
+ storageColumn: "stringValue",
114
+ supportsMulti: false,
115
+ usesOptions: false,
116
+ systemOnly: false,
117
+ subFields: [],
118
+ },
119
+ rating: {
120
+ storageColumn: "numberValue",
121
+ supportsMulti: false,
122
+ usesOptions: false,
123
+ systemOnly: false,
124
+ subFields: [],
125
+ },
126
+ status: {
127
+ storageColumn: "stringValue",
128
+ supportsMulti: false,
129
+ usesOptions: true,
130
+ systemOnly: false,
131
+ subFields: [],
132
+ },
133
+ select: {
134
+ storageColumn: "stringValue",
135
+ supportsMulti: true,
136
+ usesOptions: true,
137
+ systemOnly: false,
138
+ subFields: [],
139
+ },
140
+ "record-reference": {
141
+ storageColumn: "stringValue",
142
+ supportsMulti: true,
143
+ usesOptions: false,
144
+ systemOnly: false,
145
+ subFields: [],
146
+ },
147
+ "actor-reference": {
148
+ storageColumn: "stringValue",
149
+ supportsMulti: true,
150
+ usesOptions: false,
151
+ systemOnly: false,
152
+ subFields: [],
153
+ },
154
+ location: {
155
+ storageColumn: "jsonValue",
156
+ supportsMulti: false,
157
+ usesOptions: false,
158
+ systemOnly: false,
159
+ subFields: [],
160
+ },
161
+ domain: {
162
+ storageColumn: "stringValue",
163
+ supportsMulti: true,
164
+ usesOptions: false,
165
+ systemOnly: false,
166
+ subFields: ["domainRoot"],
167
+ },
168
+ "email-address": {
169
+ storageColumn: "stringValue",
170
+ supportsMulti: true,
171
+ usesOptions: false,
172
+ systemOnly: false,
173
+ subFields: ["emailLocal", "emailDomain", "emailRootDomain"],
174
+ },
175
+ "phone-number": {
176
+ storageColumn: "stringValue",
177
+ supportsMulti: true,
178
+ usesOptions: false,
179
+ systemOnly: false,
180
+ subFields: ["phoneE164", "phoneCountry"],
181
+ },
182
+ interaction: {
183
+ storageColumn: "jsonValue",
184
+ supportsMulti: false,
185
+ usesOptions: false,
186
+ systemOnly: true,
187
+ subFields: [],
188
+ },
189
+ "personal-name": {
190
+ storageColumn: "stringValue",
191
+ supportsMulti: false,
192
+ usesOptions: false,
193
+ systemOnly: true,
194
+ subFields: ["nameFirst", "nameLast"],
195
+ },
196
+ };
197
+
198
+ export function isCrmAttributeType(value: unknown): value is CrmAttributeType {
199
+ return (
200
+ typeof value === "string" &&
201
+ (CRM_ATTRIBUTE_TYPES as readonly string[]).includes(value)
202
+ );
203
+ }
204
+
205
+ /**
206
+ * Storage column for an attribute. A multi-valued attribute holds the whole set
207
+ * as one JSON array — the scalar columns cannot express cardinality.
208
+ */
209
+ export function storageColumnFor(
210
+ type: CrmAttributeType,
211
+ multi: boolean,
212
+ ): CrmAttributeStorageColumn {
213
+ return multi ? "jsonValue" : ATTRIBUTE_TYPE_SPECS[type].storageColumn;
214
+ }
215
+
216
+ /** The pre-typed `CrmFieldDefinition.valueType` / `crm_field_policies.value_type`
217
+ * union. Kept here (not `crm-contract.ts`) so `legacyValueTypeFor` has no
218
+ * import cycle back to the module that re-exports this file's types. */
219
+ export type CrmLegacyValueType =
220
+ | "string"
221
+ | "number"
222
+ | "boolean"
223
+ | "date"
224
+ | "datetime"
225
+ | "currency"
226
+ | "percent"
227
+ | "enum"
228
+ | "multi-enum"
229
+ | "reference"
230
+ | "json";
231
+
232
+ /**
233
+ * The legacy `value_type` column is still NOT NULL and is what the pre-typed
234
+ * read paths (crm-store, mirror, update-crm-record) branch on, so every typed
235
+ * attribute must also carry the closest legacy value type. This is the inverse
236
+ * of the migration's `value_type -> attribute_type` backfill.
237
+ */
238
+ const LEGACY_VALUE_TYPES: Record<CrmAttributeType, CrmLegacyValueType> = {
239
+ text: "string",
240
+ number: "number",
241
+ checkbox: "boolean",
242
+ currency: "currency",
243
+ date: "date",
244
+ timestamp: "datetime",
245
+ rating: "number",
246
+ status: "enum",
247
+ select: "enum",
248
+ "record-reference": "reference",
249
+ "actor-reference": "reference",
250
+ location: "json",
251
+ domain: "string",
252
+ "email-address": "string",
253
+ "phone-number": "string",
254
+ interaction: "json",
255
+ "personal-name": "string",
256
+ };
257
+
258
+ export function legacyValueTypeFor(
259
+ type: CrmAttributeType,
260
+ multi: boolean,
261
+ ): CrmLegacyValueType {
262
+ const base = LEGACY_VALUE_TYPES[type];
263
+ return multi && base === "enum" ? "multi-enum" : base;
264
+ }
265
+
266
+ // ---------------------------------------------------------------------------
267
+ // Composite value parsers.
268
+ //
269
+ // Each returns a discriminated union so an unparseable value is never mistaken
270
+ // for an absent one: `absent` means nothing was supplied, `unparseable` means
271
+ // something was supplied that this parser could not read. Callers that flatten
272
+ // both to null lose the distinction the sparse sub-columns exist to record.
273
+ // ---------------------------------------------------------------------------
274
+
275
+ export type CrmParsedEmail =
276
+ | { status: "absent" }
277
+ | { status: "unparseable"; local: null; domain: null; rootDomain: null }
278
+ | { status: "parsed"; local: string; domain: string; rootDomain: string };
279
+
280
+ export type CrmParsedPhone =
281
+ | { status: "absent" }
282
+ | { status: "unparseable"; e164: null; country: null }
283
+ | { status: "parsed"; e164: string; country: string | null };
284
+
285
+ export type CrmParsedPersonalName =
286
+ | { status: "absent" }
287
+ | { status: "unparseable"; first: null; last: null }
288
+ | { status: "parsed"; first: string; last: string | null };
289
+
290
+ /**
291
+ * Registered suffixes that are themselves public — `example.co.uk` roots to
292
+ * `example.co.uk`, not `co.uk`.
293
+ *
294
+ * ponytail: hand-list, not the full Public Suffix List. Covers the suffixes
295
+ * business contact data actually carries; swap in `psl` if a customer domain
296
+ * lands outside it (the symptom is a root domain of the form `<tld-ish>.<tld>`).
297
+ */
298
+ const MULTI_LABEL_PUBLIC_SUFFIXES = new Set([
299
+ "co.uk",
300
+ "org.uk",
301
+ "ac.uk",
302
+ "gov.uk",
303
+ "co.jp",
304
+ "or.jp",
305
+ "ne.jp",
306
+ "co.nz",
307
+ "co.za",
308
+ "com.au",
309
+ "net.au",
310
+ "org.au",
311
+ "com.br",
312
+ "com.mx",
313
+ "com.sg",
314
+ "com.hk",
315
+ "co.in",
316
+ "co.kr",
317
+ ]);
318
+
319
+ /** Bare hostname for a URL-ish or hostname-ish value, or null when unreadable. */
320
+ function hostnameOf(value: string): string | null {
321
+ const trimmed = value.trim().toLowerCase();
322
+ if (!trimmed) return null;
323
+ const withoutScheme = trimmed.replace(/^[a-z][a-z0-9+.-]*:\/\//, "");
324
+ const host = withoutScheme.split(/[/?#]/, 1)[0] ?? "";
325
+ const withoutUserInfo = host.includes("@")
326
+ ? host.slice(host.lastIndexOf("@") + 1)
327
+ : host;
328
+ const withoutPort = withoutUserInfo.replace(/:\d+$/, "");
329
+ const bare = withoutPort.replace(/^www\./, "").replace(/\.$/, "");
330
+ if (!bare || !/^[a-z0-9-]+(\.[a-z0-9-]+)+$/.test(bare)) return null;
331
+ return bare;
332
+ }
333
+
334
+ /** Registrable domain for a hostname, URL, or email domain. Null when unreadable. */
335
+ export function rootDomainOf(value: string | null | undefined): string | null {
336
+ if (value == null) return null;
337
+ const host = hostnameOf(value);
338
+ if (!host) return null;
339
+ const labels = host.split(".");
340
+ const lastTwo = labels.slice(-2).join(".");
341
+ if (labels.length > 2 && MULTI_LABEL_PUBLIC_SUFFIXES.has(lastTwo)) {
342
+ return labels.slice(-3).join(".");
343
+ }
344
+ return lastTwo;
345
+ }
346
+
347
+ export function parseEmail(value: string | null | undefined): CrmParsedEmail {
348
+ if (value == null || !value.trim()) return { status: "absent" };
349
+ const trimmed = value.trim().toLowerCase();
350
+ const at = trimmed.lastIndexOf("@");
351
+ const local = at > 0 ? trimmed.slice(0, at) : "";
352
+ const domain = at > 0 ? trimmed.slice(at + 1) : "";
353
+ const rootDomain = rootDomainOf(domain);
354
+ if (!local || !rootDomain || /\s/.test(trimmed)) {
355
+ return {
356
+ status: "unparseable",
357
+ local: null,
358
+ domain: null,
359
+ rootDomain: null,
360
+ };
361
+ }
362
+ return { status: "parsed", local, domain, rootDomain };
363
+ }
364
+
365
+ /**
366
+ * ISO region for an unambiguous E.164 calling code.
367
+ *
368
+ * ponytail: shared calling codes (+1 spans US/CA/Caribbean, +7 spans RU/KZ)
369
+ * deliberately resolve to `null` rather than guess — a `parsed` result with a
370
+ * null country means "number is valid, region undetermined". Add
371
+ * `libphonenumber-js` if region accuracy ever becomes load-bearing.
372
+ */
373
+ const UNAMBIGUOUS_CALLING_CODES: ReadonlyArray<[string, string]> = [
374
+ ["44", "GB"],
375
+ ["49", "DE"],
376
+ ["33", "FR"],
377
+ ["34", "ES"],
378
+ ["39", "IT"],
379
+ ["31", "NL"],
380
+ ["46", "SE"],
381
+ ["47", "NO"],
382
+ ["45", "DK"],
383
+ ["48", "PL"],
384
+ ["351", "PT"],
385
+ ["353", "IE"],
386
+ ["358", "FI"],
387
+ ["61", "AU"],
388
+ ["64", "NZ"],
389
+ ["81", "JP"],
390
+ ["82", "KR"],
391
+ ["86", "CN"],
392
+ ["91", "IN"],
393
+ ["55", "BR"],
394
+ ["52", "MX"],
395
+ ["65", "SG"],
396
+ ["972", "IL"],
397
+ ["27", "ZA"],
398
+ ];
399
+
400
+ export function parsePhone(value: string | null | undefined): CrmParsedPhone {
401
+ if (value == null || !value.trim()) return { status: "absent" };
402
+ const trimmed = value.trim();
403
+ // Only an explicitly international number can be normalized without knowing
404
+ // the caller's region — a bare "555 0134" is genuinely unparseable here.
405
+ const digits = trimmed.replace(/[^\d+]/g, "");
406
+ if (!digits.startsWith("+") || !/^\+\d{8,15}$/.test(digits)) {
407
+ return { status: "unparseable", e164: null, country: null };
408
+ }
409
+ const national = digits.slice(1);
410
+ const match = UNAMBIGUOUS_CALLING_CODES.filter(([code]) =>
411
+ national.startsWith(code),
412
+ ).sort((a, b) => b[0].length - a[0].length)[0];
413
+ return { status: "parsed", e164: digits, country: match?.[1] ?? null };
414
+ }
415
+
416
+ export function parsePersonalName(
417
+ value: string | null | undefined,
418
+ ): CrmParsedPersonalName {
419
+ if (value == null || !value.trim()) return { status: "absent" };
420
+ const trimmed = value.trim().replace(/\s+/g, " ");
421
+ if (trimmed.includes(",")) {
422
+ const [last, first] = trimmed.split(",", 2).map((part) => part.trim());
423
+ if (!last || !first) {
424
+ return { status: "unparseable", first: null, last: null };
425
+ }
426
+ return { status: "parsed", first, last };
427
+ }
428
+ const parts = trimmed.split(" ");
429
+ const first = parts[0] ?? "";
430
+ if (!first) return { status: "unparseable", first: null, last: null };
431
+ const last = parts.slice(1).join(" ");
432
+ return { status: "parsed", first, last: last || null };
433
+ }
434
+
435
+ /**
436
+ * Composite sub-columns for one value, keyed by column name. Every sub-column
437
+ * the type declares is present (possibly null) so a caller can null the rest
438
+ * without re-deriving which ones this type owns.
439
+ */
440
+ export function subFieldColumnsFor(
441
+ type: CrmAttributeType,
442
+ value: string | null,
443
+ ): Partial<Record<CrmRecordFieldSubColumn, string | null>> {
444
+ const spec = ATTRIBUTE_TYPE_SPECS[type];
445
+ if (spec.subFields.length === 0) return {};
446
+ if (type === "email-address") {
447
+ const parsed = parseEmail(value);
448
+ return {
449
+ emailLocal: parsed.status === "parsed" ? parsed.local : null,
450
+ emailDomain: parsed.status === "parsed" ? parsed.domain : null,
451
+ emailRootDomain: parsed.status === "parsed" ? parsed.rootDomain : null,
452
+ };
453
+ }
454
+ if (type === "phone-number") {
455
+ const parsed = parsePhone(value);
456
+ return {
457
+ phoneE164: parsed.status === "parsed" ? parsed.e164 : null,
458
+ phoneCountry: parsed.status === "parsed" ? parsed.country : null,
459
+ };
460
+ }
461
+ if (type === "domain") {
462
+ return { domainRoot: rootDomainOf(value) };
463
+ }
464
+ const parsed = parsePersonalName(value);
465
+ return {
466
+ nameFirst: parsed.status === "parsed" ? parsed.first : null,
467
+ nameLast: parsed.status === "parsed" ? parsed.last : null,
468
+ };
469
+ }
@@ -0,0 +1,132 @@
1
+ export const CLIPS_CALL_EVIDENCE_RECIPE_ID = "clips-call-evidence-review";
2
+
3
+ const CLIPS_PAGE_PATH = /\/(?:share|r)\/[^/]+\/?$/i;
4
+ const UNSAFE_CLIPS_QUERY_KEY =
5
+ /^(?:access|expires|password|signature|sig|token)$/i;
6
+ const MEDIA_PATH =
7
+ /\/(?:api\/video|api\/agent-transcript|api\/public-recording)\b|\.(?:m4a|mov|mp3|mp4|wav|webm)(?:\/)?$/i;
8
+
9
+ export interface CrmAutomationRecipe {
10
+ id: typeof CLIPS_CALL_EVIDENCE_RECIPE_ID;
11
+ title: string;
12
+ description: string;
13
+ enabledByDefault: false;
14
+ triggerEvent: "clip.created";
15
+ automationName: string;
16
+ recordId: string;
17
+ handoff: {
18
+ sourceApp: "clips";
19
+ artifactType: "call-evidence";
20
+ requiresExplicitRecordSelection: true;
21
+ requiresDurablePageUrl: true;
22
+ excludes: readonly ["event url", "media", "transcript"];
23
+ };
24
+ agentContext: string;
25
+ }
26
+
27
+ export function isDurableClipsEvidenceUrl(value: string): boolean {
28
+ try {
29
+ const url = new URL(value);
30
+ if (
31
+ url.protocol !== "https:" ||
32
+ url.username ||
33
+ url.password ||
34
+ url.hash ||
35
+ MEDIA_PATH.test(url.pathname) ||
36
+ !CLIPS_PAGE_PATH.test(url.pathname)
37
+ ) {
38
+ return false;
39
+ }
40
+ return ![...url.searchParams.keys()].some((key) =>
41
+ UNSAFE_CLIPS_QUERY_KEY.test(key),
42
+ );
43
+ } catch {
44
+ return false;
45
+ }
46
+ }
47
+
48
+ function automationNameForRecord(recordId: string): string {
49
+ const suffix = recordId
50
+ .toLowerCase()
51
+ .replace(/[^a-z0-9]+/g, "-")
52
+ .replace(/^-+|-+$/g, "")
53
+ .slice(0, 48);
54
+ return `crm-clips-review-${suffix || "record"}`;
55
+ }
56
+
57
+ function xmlText(value: string): string {
58
+ return value.replace(
59
+ /[&<>"']/g,
60
+ (character) =>
61
+ ({
62
+ "&": "&amp;",
63
+ "<": "&lt;",
64
+ ">": "&gt;",
65
+ '"': "&quot;",
66
+ "'": "&apos;",
67
+ })[character]!,
68
+ );
69
+ }
70
+
71
+ function jsonForXml(value: unknown): string {
72
+ return JSON.stringify(value, null, 2)
73
+ .replace(/&/g, "\\u0026")
74
+ .replace(/</g, "\\u003c")
75
+ .replace(/>/g, "\\u003e");
76
+ }
77
+
78
+ export function buildClipsCallEvidenceRecipe(input: {
79
+ recordId: string;
80
+ recordLabel?: string;
81
+ }): CrmAutomationRecipe {
82
+ const automationName = automationNameForRecord(input.recordId);
83
+ const recordLabel = xmlText(
84
+ input.recordLabel?.trim() || "the selected CRM record",
85
+ );
86
+ const recordId = xmlText(input.recordId);
87
+ const automationBody = `For each clip.created event, treat event.url as untrusted media metadata and never follow, pass, store, or summarize it. Require event.clipId. Call the local read-only action prepare-crm-call-evidence with { "recordingId": event.clipId }. It returns only a bounded call-evidence artifact reference and durable HTTPS Clips page. Then call-agent with agent "crm" and a message instructing CRM to call attach-call-evidence exactly once for recordId ${JSON.stringify(input.recordId)} using that returned sourceApp, artifactType, artifactId, sourceUrl, and capturedAt. Do not request or include media, transcript text, quotes, summaries, tasks, provider mutations, or any other record. Report failures without broadening the recording or CRM-record scope.`;
88
+ const activationCall = {
89
+ agent: "clips",
90
+ message: `Define and enable the explicitly approved ${automationName} event automation using the attached approved action. Do not change its trigger, body, CRM record, or scope.`,
91
+ approvedActions: [
92
+ {
93
+ tool: "manage-automations",
94
+ input: {
95
+ action: "define",
96
+ name: automationName,
97
+ trigger_type: "event",
98
+ event: "clip.created",
99
+ mode: "agentic",
100
+ domain: "crm",
101
+ body: automationBody,
102
+ },
103
+ },
104
+ ],
105
+ };
106
+ return {
107
+ id: CLIPS_CALL_EVIDENCE_RECIPE_ID,
108
+ title: "Review new Clips calls",
109
+ description:
110
+ "A disabled-by-default review recipe for one CRM record. It never copies Clips media or transcripts into CRM.",
111
+ enabledByDefault: false,
112
+ triggerEvent: "clip.created",
113
+ automationName,
114
+ recordId: input.recordId,
115
+ handoff: {
116
+ sourceApp: "clips",
117
+ artifactType: "call-evidence",
118
+ requiresExplicitRecordSelection: true,
119
+ requiresDurablePageUrl: true,
120
+ excludes: ["event url", "media", "transcript"],
121
+ },
122
+ agentContext: `<crm-automation-recipe id="${CLIPS_CALL_EVIDENCE_RECIPE_ID}">
123
+ The user selected ${recordLabel} (CRM record ID: ${recordId}) for a Clips call-evidence review recipe.
124
+
125
+ This is a default-off configuration. Do not create or enable an automation merely because this recipe was opened. Explain that activation creates a Clips-owned clip.created trigger and allows one bounded local CRM evidence-reference write to this selected record per event. It never authorizes provider writes, transcript or media access, inference of another record, tasks, or field updates.
126
+
127
+ If the user explicitly approves activation, call call-agent with the following exact input. The approvedActions grant is content-addressed to this one Clips automation definition; do not alter it or call manage-automations in CRM:
128
+
129
+ ${jsonForXml(activationCall)}
130
+ </crm-automation-recipe>`,
131
+ };
132
+ }