@open-mercato/core 0.5.1-develop.3036.f02c281f23 → 0.5.1-develop.3043.1a796c3920

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 (163) hide show
  1. package/.turbo/turbo-build.log +1 -1
  2. package/AGENTS.md +13 -1
  3. package/dist/helpers/integration/api.js +29 -16
  4. package/dist/helpers/integration/api.js.map +2 -2
  5. package/dist/helpers/integration/auth.js +11 -6
  6. package/dist/helpers/integration/auth.js.map +3 -3
  7. package/dist/modules/auth/commands/roles.js +9 -12
  8. package/dist/modules/auth/commands/roles.js.map +2 -2
  9. package/dist/modules/catalog/ai-agents-context.js +147 -0
  10. package/dist/modules/catalog/ai-agents-context.js.map +7 -0
  11. package/dist/modules/catalog/ai-agents.js +383 -0
  12. package/dist/modules/catalog/ai-agents.js.map +7 -0
  13. package/dist/modules/catalog/ai-tools/_shared.js +318 -0
  14. package/dist/modules/catalog/ai-tools/_shared.js.map +7 -0
  15. package/dist/modules/catalog/ai-tools/authoring-pack.js +391 -0
  16. package/dist/modules/catalog/ai-tools/authoring-pack.js.map +7 -0
  17. package/dist/modules/catalog/ai-tools/categories-pack.js +167 -0
  18. package/dist/modules/catalog/ai-tools/categories-pack.js.map +7 -0
  19. package/dist/modules/catalog/ai-tools/configuration-pack.js +120 -0
  20. package/dist/modules/catalog/ai-tools/configuration-pack.js.map +7 -0
  21. package/dist/modules/catalog/ai-tools/media-tags-pack.js +107 -0
  22. package/dist/modules/catalog/ai-tools/media-tags-pack.js.map +7 -0
  23. package/dist/modules/catalog/ai-tools/merchandising-pack.js +429 -0
  24. package/dist/modules/catalog/ai-tools/merchandising-pack.js.map +7 -0
  25. package/dist/modules/catalog/ai-tools/mutation-pack.js +576 -0
  26. package/dist/modules/catalog/ai-tools/mutation-pack.js.map +7 -0
  27. package/dist/modules/catalog/ai-tools/prices-offers-pack.js +208 -0
  28. package/dist/modules/catalog/ai-tools/prices-offers-pack.js.map +7 -0
  29. package/dist/modules/catalog/ai-tools/products-pack.js +298 -0
  30. package/dist/modules/catalog/ai-tools/products-pack.js.map +7 -0
  31. package/dist/modules/catalog/ai-tools/stats-pack.js +57 -0
  32. package/dist/modules/catalog/ai-tools/stats-pack.js.map +7 -0
  33. package/dist/modules/catalog/ai-tools/types.js +10 -0
  34. package/dist/modules/catalog/ai-tools/types.js.map +7 -0
  35. package/dist/modules/catalog/ai-tools/variants-pack.js +75 -0
  36. package/dist/modules/catalog/ai-tools/variants-pack.js.map +7 -0
  37. package/dist/modules/catalog/ai-tools.js +28 -0
  38. package/dist/modules/catalog/ai-tools.js.map +7 -0
  39. package/dist/modules/catalog/backend/catalog/products/MerchandisingAssistantSheet.js +466 -0
  40. package/dist/modules/catalog/backend/catalog/products/MerchandisingAssistantSheet.js.map +7 -0
  41. package/dist/modules/catalog/backend/catalog/products/page.js +7 -1
  42. package/dist/modules/catalog/backend/catalog/products/page.js.map +2 -2
  43. package/dist/modules/catalog/components/CatalogStatsCard.js +91 -0
  44. package/dist/modules/catalog/components/CatalogStatsCard.js.map +7 -0
  45. package/dist/modules/catalog/components/products/ProductsDataTable.js +23 -3
  46. package/dist/modules/catalog/components/products/ProductsDataTable.js.map +2 -2
  47. package/dist/modules/catalog/events.js +7 -4
  48. package/dist/modules/catalog/events.js.map +2 -2
  49. package/dist/modules/catalog/widgets/injection/merchandising-assistant-trigger/widget.client.js +59 -0
  50. package/dist/modules/catalog/widgets/injection/merchandising-assistant-trigger/widget.client.js.map +7 -0
  51. package/dist/modules/catalog/widgets/injection/merchandising-assistant-trigger/widget.js +17 -0
  52. package/dist/modules/catalog/widgets/injection/merchandising-assistant-trigger/widget.js.map +7 -0
  53. package/dist/modules/catalog/widgets/injection/product-seo/widget.client.js +1 -1
  54. package/dist/modules/catalog/widgets/injection/product-seo/widget.client.js.map +2 -2
  55. package/dist/modules/catalog/widgets/injection-table.js +13 -1
  56. package/dist/modules/catalog/widgets/injection-table.js.map +2 -2
  57. package/dist/modules/customer_accounts/widgets/injection/portal-ai-assistant-trigger/widget.client.js +94 -0
  58. package/dist/modules/customer_accounts/widgets/injection/portal-ai-assistant-trigger/widget.client.js.map +7 -0
  59. package/dist/modules/customer_accounts/widgets/injection/portal-ai-assistant-trigger/widget.js +17 -0
  60. package/dist/modules/customer_accounts/widgets/injection/portal-ai-assistant-trigger/widget.js.map +7 -0
  61. package/dist/modules/customer_accounts/widgets/injection-table.js +9 -0
  62. package/dist/modules/customer_accounts/widgets/injection-table.js.map +2 -2
  63. package/dist/modules/customers/ai-agents-context.js +96 -0
  64. package/dist/modules/customers/ai-agents-context.js.map +7 -0
  65. package/dist/modules/customers/ai-agents.js +244 -0
  66. package/dist/modules/customers/ai-agents.js.map +7 -0
  67. package/dist/modules/customers/ai-tools/activities-tasks-pack.js +1015 -0
  68. package/dist/modules/customers/ai-tools/activities-tasks-pack.js.map +7 -0
  69. package/dist/modules/customers/ai-tools/addresses-tags-pack.js +134 -0
  70. package/dist/modules/customers/ai-tools/addresses-tags-pack.js.map +7 -0
  71. package/dist/modules/customers/ai-tools/companies-pack.js +249 -0
  72. package/dist/modules/customers/ai-tools/companies-pack.js.map +7 -0
  73. package/dist/modules/customers/ai-tools/deals-pack.js +348 -0
  74. package/dist/modules/customers/ai-tools/deals-pack.js.map +7 -0
  75. package/dist/modules/customers/ai-tools/people-pack.js +261 -0
  76. package/dist/modules/customers/ai-tools/people-pack.js.map +7 -0
  77. package/dist/modules/customers/ai-tools/settings-pack.js +102 -0
  78. package/dist/modules/customers/ai-tools/settings-pack.js.map +7 -0
  79. package/dist/modules/customers/ai-tools/types.js +10 -0
  80. package/dist/modules/customers/ai-tools/types.js.map +7 -0
  81. package/dist/modules/customers/ai-tools.js +20 -0
  82. package/dist/modules/customers/ai-tools.js.map +7 -0
  83. package/dist/modules/customers/widgets/injection/ai-assistant-trigger/widget.client.js +469 -0
  84. package/dist/modules/customers/widgets/injection/ai-assistant-trigger/widget.client.js.map +7 -0
  85. package/dist/modules/customers/widgets/injection/ai-assistant-trigger/widget.js +17 -0
  86. package/dist/modules/customers/widgets/injection/ai-assistant-trigger/widget.js.map +7 -0
  87. package/dist/modules/customers/widgets/injection/ai-deal-detail-trigger/widget.client.js +117 -0
  88. package/dist/modules/customers/widgets/injection/ai-deal-detail-trigger/widget.client.js.map +7 -0
  89. package/dist/modules/customers/widgets/injection/ai-deal-detail-trigger/widget.js +17 -0
  90. package/dist/modules/customers/widgets/injection/ai-deal-detail-trigger/widget.js.map +7 -0
  91. package/dist/modules/customers/widgets/injection-table.js +26 -0
  92. package/dist/modules/customers/widgets/injection-table.js.map +7 -0
  93. package/dist/modules/inbox_ops/ai-tools.js +4 -0
  94. package/dist/modules/inbox_ops/ai-tools.js.map +2 -2
  95. package/dist/modules/inbox_ops/lib/llmProvider.js +52 -7
  96. package/dist/modules/inbox_ops/lib/llmProvider.js.map +2 -2
  97. package/dist/modules/notifications/setup.js +13 -0
  98. package/dist/modules/notifications/setup.js.map +7 -0
  99. package/jest.config.cjs +1 -0
  100. package/jest.setup.ts +18 -0
  101. package/package.json +5 -3
  102. package/src/helpers/integration/api.ts +38 -16
  103. package/src/helpers/integration/auth.ts +13 -6
  104. package/src/modules/auth/commands/roles.ts +10 -12
  105. package/src/modules/catalog/AGENTS.md +11 -0
  106. package/src/modules/catalog/ai-agents-context.ts +239 -0
  107. package/src/modules/catalog/ai-agents.ts +525 -0
  108. package/src/modules/catalog/ai-tools/_shared.ts +487 -0
  109. package/src/modules/catalog/ai-tools/authoring-pack.ts +600 -0
  110. package/src/modules/catalog/ai-tools/categories-pack.ts +192 -0
  111. package/src/modules/catalog/ai-tools/configuration-pack.ts +218 -0
  112. package/src/modules/catalog/ai-tools/media-tags-pack.ts +127 -0
  113. package/src/modules/catalog/ai-tools/merchandising-pack.ts +608 -0
  114. package/src/modules/catalog/ai-tools/mutation-pack.ts +761 -0
  115. package/src/modules/catalog/ai-tools/prices-offers-pack.ts +376 -0
  116. package/src/modules/catalog/ai-tools/products-pack.ts +387 -0
  117. package/src/modules/catalog/ai-tools/stats-pack.ts +84 -0
  118. package/src/modules/catalog/ai-tools/types.ts +81 -0
  119. package/src/modules/catalog/ai-tools/variants-pack.ts +147 -0
  120. package/src/modules/catalog/ai-tools.ts +78 -0
  121. package/src/modules/catalog/backend/catalog/products/MerchandisingAssistantSheet.tsx +597 -0
  122. package/src/modules/catalog/backend/catalog/products/page.tsx +23 -2
  123. package/src/modules/catalog/components/CatalogStatsCard.tsx +118 -0
  124. package/src/modules/catalog/components/products/ProductsDataTable.tsx +54 -6
  125. package/src/modules/catalog/events.ts +7 -4
  126. package/src/modules/catalog/i18n/de.json +17 -0
  127. package/src/modules/catalog/i18n/en.json +17 -0
  128. package/src/modules/catalog/i18n/es.json +17 -0
  129. package/src/modules/catalog/i18n/pl.json +17 -0
  130. package/src/modules/catalog/widgets/injection/merchandising-assistant-trigger/widget.client.tsx +109 -0
  131. package/src/modules/catalog/widgets/injection/merchandising-assistant-trigger/widget.ts +29 -0
  132. package/src/modules/catalog/widgets/injection/product-seo/widget.client.tsx +1 -1
  133. package/src/modules/catalog/widgets/injection-table.ts +12 -0
  134. package/src/modules/customer_accounts/i18n/de.json +5 -0
  135. package/src/modules/customer_accounts/i18n/en.json +5 -0
  136. package/src/modules/customer_accounts/i18n/es.json +5 -0
  137. package/src/modules/customer_accounts/i18n/pl.json +5 -0
  138. package/src/modules/customer_accounts/widgets/injection/portal-ai-assistant-trigger/widget.client.tsx +136 -0
  139. package/src/modules/customer_accounts/widgets/injection/portal-ai-assistant-trigger/widget.ts +43 -0
  140. package/src/modules/customer_accounts/widgets/injection-table.ts +9 -0
  141. package/src/modules/customers/AGENTS.md +13 -0
  142. package/src/modules/customers/ai-agents-context.ts +150 -0
  143. package/src/modules/customers/ai-agents.ts +355 -0
  144. package/src/modules/customers/ai-tools/activities-tasks-pack.ts +1248 -0
  145. package/src/modules/customers/ai-tools/addresses-tags-pack.ts +145 -0
  146. package/src/modules/customers/ai-tools/companies-pack.ts +362 -0
  147. package/src/modules/customers/ai-tools/deals-pack.ts +505 -0
  148. package/src/modules/customers/ai-tools/people-pack.ts +369 -0
  149. package/src/modules/customers/ai-tools/settings-pack.ts +121 -0
  150. package/src/modules/customers/ai-tools/types.ts +76 -0
  151. package/src/modules/customers/ai-tools.ts +34 -0
  152. package/src/modules/customers/i18n/de.json +25 -0
  153. package/src/modules/customers/i18n/en.json +25 -0
  154. package/src/modules/customers/i18n/es.json +25 -0
  155. package/src/modules/customers/i18n/pl.json +25 -0
  156. package/src/modules/customers/widgets/injection/ai-assistant-trigger/widget.client.tsx +580 -0
  157. package/src/modules/customers/widgets/injection/ai-assistant-trigger/widget.ts +36 -0
  158. package/src/modules/customers/widgets/injection/ai-deal-detail-trigger/widget.client.tsx +191 -0
  159. package/src/modules/customers/widgets/injection/ai-deal-detail-trigger/widget.ts +37 -0
  160. package/src/modules/customers/widgets/injection-table.ts +41 -0
  161. package/src/modules/inbox_ops/ai-tools.ts +4 -0
  162. package/src/modules/inbox_ops/lib/llmProvider.ts +83 -7
  163. package/src/modules/notifications/setup.ts +11 -0
@@ -0,0 +1,136 @@
1
+ "use client"
2
+
3
+ /**
4
+ * Step 4.10 — Portal AiChat injection widget (client).
5
+ *
6
+ * Renders an "Ask AI" trigger button + sheet inside the portal profile
7
+ * page's `portal:profile:after` injection spot. Clicking the trigger
8
+ * opens a right-side sheet embedding `<AiChat>` wired to
9
+ * `customers.account_assistant`.
10
+ *
11
+ * Feature-gating:
12
+ * - Declared in `widget.ts` metadata (`portal.account.manage`).
13
+ * - The widget ALSO self-checks `context.resolvedFeatures` as a
14
+ * defense-in-depth measure so the button never renders for a
15
+ * customer who lacks the feature (portal pages are themselves
16
+ * feature-gated, but the injection registry does not currently
17
+ * enforce metadata.features at render time).
18
+ *
19
+ * `pageContext` shape:
20
+ * { view: 'portal.profile', recordType: 'customer', recordId: <userId|null>, extra: {} }
21
+ */
22
+
23
+ import * as React from 'react'
24
+ import { Sparkles } from 'lucide-react'
25
+ import { AiChat } from '@open-mercato/ui/ai/AiChat'
26
+ import { Button } from '@open-mercato/ui/primitives/button'
27
+ import {
28
+ Dialog,
29
+ DialogContent,
30
+ DialogDescription,
31
+ DialogHeader,
32
+ DialogTitle,
33
+ } from '@open-mercato/ui/primitives/dialog'
34
+ import { useT } from '@open-mercato/shared/lib/i18n/context'
35
+ import { cn } from '@open-mercato/shared/lib/utils'
36
+ import { hasFeature } from '@open-mercato/shared/security/features'
37
+
38
+ export const PORTAL_AI_INJECT_AGENT_ID = 'customers.account_assistant'
39
+ export const PORTAL_AI_INJECT_REQUIRED_FEATURE = 'portal.account.manage'
40
+
41
+ export interface PortalAiInjectPageContext {
42
+ view: 'portal.profile'
43
+ recordType: 'customer'
44
+ recordId: string | null
45
+ extra: Record<string, never>
46
+ }
47
+
48
+ interface PortalInjectionContext {
49
+ orgSlug?: string
50
+ user?: { id?: string | null } | null
51
+ resolvedFeatures?: string[]
52
+ isPortalAdmin?: boolean
53
+ }
54
+
55
+ interface PortalAiAssistantTriggerProps {
56
+ context?: PortalInjectionContext
57
+ }
58
+
59
+ function readUserId(user: PortalInjectionContext['user']): string | null {
60
+ if (!user) return null
61
+ const id = user.id
62
+ return typeof id === 'string' && id.length > 0 ? id : null
63
+ }
64
+
65
+ export default function PortalAiAssistantTriggerWidget({ context }: PortalAiAssistantTriggerProps) {
66
+ const t = useT()
67
+ const [open, setOpen] = React.useState(false)
68
+
69
+ const resolvedFeatures = Array.isArray(context?.resolvedFeatures)
70
+ ? (context?.resolvedFeatures as string[])
71
+ : []
72
+ const featureAllowed =
73
+ context?.isPortalAdmin === true ||
74
+ hasFeature(resolvedFeatures, PORTAL_AI_INJECT_REQUIRED_FEATURE)
75
+
76
+ const pageContext = React.useMemo<PortalAiInjectPageContext>(() => ({
77
+ view: 'portal.profile',
78
+ recordType: 'customer',
79
+ recordId: readUserId(context?.user ?? null),
80
+ extra: {},
81
+ }), [context?.user])
82
+
83
+ if (!featureAllowed) return null
84
+
85
+ return (
86
+ <div className="mt-6" data-ai-portal-inject-wrapper="">
87
+ <Button
88
+ type="button"
89
+ variant="outline"
90
+ size="sm"
91
+ onClick={() => setOpen(true)}
92
+ data-ai-portal-inject-trigger=""
93
+ aria-label={t(
94
+ 'customer_accounts.portal_ai_assistant.trigger.ariaLabel',
95
+ 'Open portal AI assistant',
96
+ )}
97
+ >
98
+ <Sparkles className="size-4" aria-hidden />
99
+ <span>{t('customer_accounts.portal_ai_assistant.trigger.label', 'Ask AI')}</span>
100
+ </Button>
101
+ <Dialog open={open} onOpenChange={setOpen}>
102
+ <DialogContent
103
+ className={cn(
104
+ 'sm:max-w-xl sm:top-0 sm:bottom-0 sm:right-0 sm:left-auto sm:translate-x-0 sm:translate-y-0',
105
+ 'sm:h-screen sm:max-h-screen sm:rounded-none sm:rounded-l-2xl',
106
+ 'flex flex-col gap-3 p-4',
107
+ )}
108
+ data-ai-portal-inject-sheet=""
109
+ >
110
+ <DialogHeader>
111
+ <DialogTitle>
112
+ {t('customer_accounts.portal_ai_assistant.sheet.title', 'Portal AI assistant')}
113
+ </DialogTitle>
114
+ <DialogDescription>
115
+ {t(
116
+ 'customer_accounts.portal_ai_assistant.sheet.description',
117
+ 'Read-only assistant for portal customers. Ask about your account and recent activity.',
118
+ )}
119
+ </DialogDescription>
120
+ </DialogHeader>
121
+ <div className="min-h-0 flex-1" data-ai-portal-inject-chat-container="">
122
+ <AiChat
123
+ agent={PORTAL_AI_INJECT_AGENT_ID}
124
+ pageContext={pageContext as unknown as Record<string, unknown>}
125
+ className="h-full"
126
+ placeholder={t(
127
+ 'customer_accounts.portal_ai_assistant.sheet.composerPlaceholder',
128
+ 'Ask about your account...',
129
+ )}
130
+ />
131
+ </div>
132
+ </DialogContent>
133
+ </Dialog>
134
+ </div>
135
+ )
136
+ }
@@ -0,0 +1,43 @@
1
+ import type { InjectionWidgetModule } from '@open-mercato/shared/modules/widgets/injection'
2
+ import PortalAiAssistantTriggerWidget from './widget.client'
3
+
4
+ /**
5
+ * Step 4.10 — Portal AiChat injection example.
6
+ *
7
+ * Demonstrates how a third-party module can drop `<AiChat>` onto a
8
+ * portal page it does NOT own (the customer-portal profile page) via
9
+ * the existing widget-injection system. Targets spot
10
+ * `portal:profile:after` (see `PortalInjectionSpots.pageAfter('profile')`
11
+ * in `packages/ui/src/backend/injection/spotIds.ts`).
12
+ *
13
+ * The trigger opens a right-side sheet embedding
14
+ * `<AiChat agent="customers.account_assistant" pageContext={...} />`.
15
+ * `pageContext` follows the spec §10.1 shape:
16
+ *
17
+ * { view: 'portal.profile',
18
+ * recordType: 'customer',
19
+ * recordId: <customer-user-id | null>,
20
+ * extra: {} }
21
+ *
22
+ * Gated behind customer feature `portal.account.manage`, which is the
23
+ * closest existing customer-facing feature (no dedicated
24
+ * `portal.ai_assistant.view` feature exists yet — tracked as a
25
+ * follow-up gap; see Step 4.10 checks).
26
+ *
27
+ * Phase 2 ships `customers.account_assistant` as the agent (read-only)
28
+ * because no dedicated customer-portal agent has been introduced yet.
29
+ */
30
+ const widget: InjectionWidgetModule<Record<string, unknown>, Record<string, unknown>> = {
31
+ metadata: {
32
+ id: 'customer_accounts.injection.portal-ai-assistant-trigger',
33
+ title: 'Portal AI Assistant Trigger',
34
+ description:
35
+ 'Renders an "Ask AI" button on the portal profile page that opens a sheet embedding the customers account assistant.',
36
+ features: ['portal.account.manage'],
37
+ priority: 100,
38
+ enabled: true,
39
+ },
40
+ Widget: PortalAiAssistantTriggerWidget,
41
+ }
42
+
43
+ export default widget
@@ -19,6 +19,15 @@ export const injectionTable: ModuleInjectionTable = {
19
19
  priority: 200,
20
20
  },
21
21
  ],
22
+ // Step 4.10 — Portal AiChat injection example.
23
+ // Mapped to the portal profile page's `pageAfter('profile')` spot;
24
+ // third-party modules targeting other portal pages can copy this entry.
25
+ 'portal:profile:after': [
26
+ {
27
+ widgetId: 'customer_accounts.injection.portal-ai-assistant-trigger',
28
+ priority: 100,
29
+ },
30
+ ],
22
31
  }
23
32
 
24
33
  export default injectionTable
@@ -93,3 +93,16 @@ Use `collectCustomFieldValues()` from `@open-mercato/ui/backend/utils/customFiel
93
93
  ## Module Files Checklist — All MUST Be Present
94
94
 
95
95
  `acl.ts`, `ce.ts`, `di.ts`, `events.ts`, `index.ts`, `notifications.ts`, `search.ts`, `setup.ts`, `analytics.ts`, `vector.ts`
96
+
97
+ ## AI Agents in This Module
98
+
99
+ This module is the reference implementation for the AI framework. Copy `ai-agents.ts` + `ai-tools.ts` when adding AI agents to other modules. See `/framework/ai-assistant/agents` for the full guide.
100
+
101
+ | Agent ID | Mode | Policy | Purpose |
102
+ |----------|------|--------|---------|
103
+ | `customers.account_assistant` | chat | read-only (mutation-capable via per-tenant override that unlocks `customers.update_deal_stage`; see `packages/core/src/modules/customers/ai-agents.ts` for the whitelist and prompt) | Operator-facing assistant that explores people, companies, deals, activities, tasks, addresses, tags, and settings through the customers tool pack. |
104
+ | `customers.update_deal_stage` | tool (mutation) | `destructive-confirm-required` — goes through `prepareMutation` + the approval card | Moves a deal between stages / flips status between open, won, lost. Declared via `defineAiTool` in `ai-tools.ts` and exposed only when the tenant mutation-policy override promotes the agent above read-only. |
105
+
106
+ `<AiChat agent="customers.account_assistant" />` is injected in two places (both live in `widgets/injection/`):
107
+ - People list: `data-table:customers.people.list:header` via the `ai-assistant-trigger` widget.
108
+ - Deal detail: `detail:customers.deal:header` via the `ai-deal-detail-trigger` widget.
@@ -0,0 +1,150 @@
1
+ /**
2
+ * Page-context hydration helpers for `customers.account_assistant`
3
+ * (Phase 3 WS-A, Step 5.2).
4
+ *
5
+ * The `resolvePageContext` callback on the module-root `ai-agents.ts`
6
+ * delegates to this file when the incoming request carries an `entityType`
7
+ * + `recordId` combination the runtime recognises. Each helper reuses the
8
+ * same tool-pack handler the Step 3.9 pack ships (`customers.get_person`,
9
+ * `customers.get_company`, `customers.get_deal`) so there is exactly one
10
+ * loader per record type — not a second parallel query path that could
11
+ * drift from what the agent is actually allowed to call.
12
+ *
13
+ * Every helper:
14
+ * - Runs only when `tenantId` is present; cross-tenant ids return `null`
15
+ * (the tool handlers already guard tenant scope via
16
+ * `findOneWithDecryption`, but we double-check in the output).
17
+ * - Caps `includeRelated` payloads to what the tool's own cap enforces.
18
+ * - Swallows errors and returns `null` so a hydration fault NEVER breaks
19
+ * the chat request — the runtime will proceed without extra context.
20
+ */
21
+ import type { AwilixContainer } from 'awilix'
22
+ import customersAiTools from './ai-tools'
23
+ import type {
24
+ CustomersAiToolDefinition,
25
+ CustomersToolContext,
26
+ } from './ai-tools/types'
27
+
28
+ const UUID_REGEX = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i
29
+
30
+ function isUuid(value: unknown): value is string {
31
+ return typeof value === 'string' && UUID_REGEX.test(value)
32
+ }
33
+
34
+ function findTool(name: string): CustomersAiToolDefinition | null {
35
+ return (
36
+ (customersAiTools as CustomersAiToolDefinition[]).find((tool) => tool.name === name) ?? null
37
+ )
38
+ }
39
+
40
+ function buildToolContext(
41
+ container: AwilixContainer,
42
+ tenantId: string,
43
+ organizationId: string | null,
44
+ ): CustomersToolContext {
45
+ return {
46
+ tenantId,
47
+ organizationId,
48
+ userId: null,
49
+ container,
50
+ userFeatures: [],
51
+ isSuperAdmin: true,
52
+ apiKeySecret: undefined,
53
+ sessionId: undefined,
54
+ }
55
+ }
56
+
57
+ function renderContextBlock(label: string, payload: unknown): string {
58
+ return `## Page context — ${label}\n${JSON.stringify(payload, null, 2)}`
59
+ }
60
+
61
+ export interface HydrateCustomersContextInput {
62
+ entityType: string
63
+ recordId: string
64
+ container: AwilixContainer
65
+ tenantId: string | null
66
+ organizationId: string | null
67
+ }
68
+
69
+ const PERSON_ENTITY_TYPES = new Set([
70
+ 'person',
71
+ 'customers.person',
72
+ 'customers:customer_entity',
73
+ ])
74
+
75
+ const COMPANY_ENTITY_TYPES = new Set([
76
+ 'company',
77
+ 'customers.company',
78
+ ])
79
+
80
+ const DEAL_ENTITY_TYPES = new Set([
81
+ 'deal',
82
+ 'customers.deal',
83
+ ])
84
+
85
+ async function hydrateWithTool(
86
+ toolName: string,
87
+ inputArgs: Record<string, unknown>,
88
+ toolContext: CustomersToolContext,
89
+ ): Promise<unknown | null> {
90
+ const tool = findTool(toolName)
91
+ if (!tool) {
92
+ console.warn(`[customers.account_assistant] resolvePageContext: tool "${toolName}" not registered`)
93
+ return null
94
+ }
95
+ try {
96
+ const result = await tool.handler(inputArgs as never, toolContext)
97
+ if (!result || typeof result !== 'object') return null
98
+ if ((result as { found?: boolean }).found === false) return null
99
+ return result
100
+ } catch (error) {
101
+ console.warn(
102
+ `[customers.account_assistant] resolvePageContext: tool "${toolName}" failed (reason="hydration_error"); skipping`,
103
+ error instanceof Error ? error.message : error,
104
+ )
105
+ return null
106
+ }
107
+ }
108
+
109
+ export async function hydrateCustomersAccountContext(
110
+ input: HydrateCustomersContextInput,
111
+ ): Promise<string | null> {
112
+ const tenantId = input.tenantId
113
+ if (!tenantId) return null
114
+ if (!isUuid(input.recordId)) return null
115
+ const entityType = input.entityType.trim().toLowerCase()
116
+ if (!entityType) return null
117
+ const toolContext = buildToolContext(input.container, tenantId, input.organizationId)
118
+
119
+ if (PERSON_ENTITY_TYPES.has(entityType)) {
120
+ const result = await hydrateWithTool(
121
+ 'customers.get_person',
122
+ { personId: input.recordId, includeRelated: true },
123
+ toolContext,
124
+ )
125
+ if (!result) return null
126
+ return renderContextBlock(`Person ${input.recordId}`, result)
127
+ }
128
+
129
+ if (COMPANY_ENTITY_TYPES.has(entityType)) {
130
+ const result = await hydrateWithTool(
131
+ 'customers.get_company',
132
+ { companyId: input.recordId, includeRelated: true },
133
+ toolContext,
134
+ )
135
+ if (!result) return null
136
+ return renderContextBlock(`Company ${input.recordId}`, result)
137
+ }
138
+
139
+ if (DEAL_ENTITY_TYPES.has(entityType)) {
140
+ const result = await hydrateWithTool(
141
+ 'customers.get_deal',
142
+ { dealId: input.recordId, includeRelated: true },
143
+ toolContext,
144
+ )
145
+ if (!result) return null
146
+ return renderContextBlock(`Deal ${input.recordId}`, result)
147
+ }
148
+
149
+ return null
150
+ }