@open-mercato/core 0.5.1-develop.3036.f02c281f23 → 0.5.1-develop.3045.b4b3320cc2

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,391 @@
1
+ import { z } from "zod";
2
+ import { findWithDecryption } from "@open-mercato/shared/lib/encryption/find";
3
+ import { Attachment } from "@open-mercato/core/modules/attachments/data/entities";
4
+ import { assertTenantScope } from "./types.js";
5
+ import {
6
+ buildProductBundle,
7
+ buildScope,
8
+ listPriceKindsCore,
9
+ resolveAttributeSchema,
10
+ resolveEm
11
+ } from "./_shared.js";
12
+ function toJsonSchema(schema, schemaName) {
13
+ const json = z.toJSONSchema(schema);
14
+ return { ...json, title: schemaName };
15
+ }
16
+ async function loadProductContext(ctx, tenantId, productId) {
17
+ const em = resolveEm(ctx);
18
+ const result = await buildProductBundle(em, ctx, tenantId, productId);
19
+ if (!result.found) {
20
+ return { found: false, productId };
21
+ }
22
+ return { found: true, productId, bundle: result };
23
+ }
24
+ function resolvePricingService(ctx) {
25
+ try {
26
+ return ctx.container.resolve("catalogPricingService");
27
+ } catch {
28
+ return null;
29
+ }
30
+ }
31
+ const draftDescriptionFromAttributesInput = z.object({
32
+ productId: z.string().uuid().describe("Catalog product id (UUID)."),
33
+ tonePreference: z.enum(["neutral", "marketing", "technical", "short"]).optional().describe("Preferred tone for the drafted description (default `neutral`).")
34
+ });
35
+ const draftDescriptionFromAttributesProposal = z.object({
36
+ description: z.string().describe("Drafted product description the model should author from the attribute bundle."),
37
+ rationale: z.string().describe("Why the model authored this description (which attributes drove it)."),
38
+ attributesUsed: z.array(z.string()).describe("Attribute keys the model actually referenced.")
39
+ });
40
+ const DRAFT_DESCRIPTION_FROM_ATTRIBUTES_SCHEMA = "DraftDescriptionFromAttributes";
41
+ const draftDescriptionFromAttributesTool = {
42
+ name: "catalog.draft_description_from_attributes",
43
+ displayName: "Draft description from attributes",
44
+ description: "Structured-output helper for D18: returns the tenant-scoped product bundle (core fields + attributes + media metadata) alongside a JSON-Schema descriptor the surrounding agent turn uses via `runAiAgentObject` to draft a product description. The handler NEVER calls the model itself; it composes the context and the output shape.",
45
+ inputSchema: draftDescriptionFromAttributesInput,
46
+ requiredFeatures: ["catalog.products.view"],
47
+ tags: ["read", "catalog", "authoring", "merchandising"],
48
+ isMutation: false,
49
+ handler: async (rawInput, ctx) => {
50
+ const { tenantId } = assertTenantScope(ctx);
51
+ const input = draftDescriptionFromAttributesInput.parse(rawInput);
52
+ const hit = await loadProductContext(ctx, tenantId, input.productId);
53
+ if (!hit.found) {
54
+ return { found: false, productId: input.productId };
55
+ }
56
+ const tonePreference = input.tonePreference ?? "neutral";
57
+ return {
58
+ found: true,
59
+ proposal: {
60
+ description: "",
61
+ rationale: "",
62
+ attributesUsed: []
63
+ },
64
+ context: {
65
+ product: hit.bundle,
66
+ tonePreference
67
+ },
68
+ outputSchemaDescriptor: {
69
+ schemaName: DRAFT_DESCRIPTION_FROM_ATTRIBUTES_SCHEMA,
70
+ jsonSchema: toJsonSchema(draftDescriptionFromAttributesProposal, DRAFT_DESCRIPTION_FROM_ATTRIBUTES_SCHEMA)
71
+ }
72
+ };
73
+ }
74
+ };
75
+ const extractAttributesFromDescriptionInput = z.object({
76
+ productId: z.string().uuid().describe("Catalog product id (UUID)."),
77
+ descriptionOverride: z.string().min(1).optional().describe("Optional description text to analyze instead of the product's stored description.")
78
+ });
79
+ const extractAttributesFromDescriptionProposal = z.object({
80
+ attributes: z.record(z.string(), z.unknown()).describe("Attribute key \u2192 value pairs mapped from the description. Shape matches the resolved attribute schema; server re-validates before any write."),
81
+ confidence: z.number().min(0).max(1).describe("Model confidence (0..1) in the extracted attribute set as a whole."),
82
+ unmapped: z.array(z.string()).describe("Description phrases that could not be mapped to any schema attribute.")
83
+ });
84
+ const EXTRACT_ATTRIBUTES_FROM_DESCRIPTION_SCHEMA = "ExtractAttributesFromDescription";
85
+ const extractAttributesFromDescriptionTool = {
86
+ name: "catalog.extract_attributes_from_description",
87
+ displayName: "Extract attributes from description",
88
+ description: "Structured-output helper for D18: returns the product bundle, the resolved attribute schema, and the description text the model should parse. The surrounding agent turn uses `runAiAgentObject` with the emitted JSON-Schema descriptor to produce the extracted attribute map. The `attributes` output shape intentionally uses `additionalProperties: true` because tenant attribute schemas are heterogeneous; any downstream write re-validates each value against the schema authoritatively.",
89
+ inputSchema: extractAttributesFromDescriptionInput,
90
+ requiredFeatures: ["catalog.products.view"],
91
+ tags: ["read", "catalog", "authoring", "merchandising"],
92
+ isMutation: false,
93
+ handler: async (rawInput, ctx) => {
94
+ const { tenantId } = assertTenantScope(ctx);
95
+ const input = extractAttributesFromDescriptionInput.parse(rawInput);
96
+ const hit = await loadProductContext(ctx, tenantId, input.productId);
97
+ if (!hit.found) {
98
+ return { found: false, productId: input.productId };
99
+ }
100
+ const attributeSchema = await resolveAttributeSchema(
101
+ ctx,
102
+ tenantId,
103
+ input.productId,
104
+ void 0
105
+ );
106
+ const description = input.descriptionOverride ?? hit.bundle.product.description ?? "";
107
+ return {
108
+ found: true,
109
+ proposal: {
110
+ attributes: {},
111
+ confidence: 0,
112
+ unmapped: []
113
+ },
114
+ context: {
115
+ product: hit.bundle,
116
+ attributeSchema,
117
+ description
118
+ },
119
+ outputSchemaDescriptor: {
120
+ schemaName: EXTRACT_ATTRIBUTES_FROM_DESCRIPTION_SCHEMA,
121
+ jsonSchema: toJsonSchema(
122
+ extractAttributesFromDescriptionProposal,
123
+ EXTRACT_ATTRIBUTES_FROM_DESCRIPTION_SCHEMA
124
+ )
125
+ }
126
+ };
127
+ }
128
+ };
129
+ const draftDescriptionFromMediaInput = z.object({
130
+ productId: z.string().uuid().describe("Catalog product id (UUID)."),
131
+ userUploadedAttachmentIds: z.array(z.string().uuid()).max(20).optional().describe("Additional attachment IDs the user just uploaded. Cross-tenant IDs are dropped with a warning (not surfaced).")
132
+ });
133
+ const draftDescriptionFromMediaProposal = z.object({
134
+ description: z.string().describe("Drafted description based on the media references."),
135
+ features: z.array(z.string()).describe("Salient product features the model surfaced from the media."),
136
+ mediaReferences: z.array(
137
+ z.object({
138
+ attachmentId: z.string(),
139
+ note: z.string().optional()
140
+ })
141
+ ).describe("Attachment IDs the model actually used, with an optional note per reference.")
142
+ });
143
+ const DRAFT_DESCRIPTION_FROM_MEDIA_SCHEMA = "DraftDescriptionFromMedia";
144
+ async function loadUserMedia(em, ctx, tenantId, attachmentIds) {
145
+ if (!attachmentIds.length) return [];
146
+ const unique = Array.from(new Set(attachmentIds));
147
+ const where = {
148
+ tenantId,
149
+ id: { $in: unique }
150
+ };
151
+ if (ctx.organizationId) where.organizationId = ctx.organizationId;
152
+ const rows = await findWithDecryption(
153
+ em,
154
+ Attachment,
155
+ where,
156
+ { limit: unique.length },
157
+ buildScope(ctx, tenantId)
158
+ );
159
+ const tenantScoped = rows.filter((row) => (row.tenantId ?? null) === tenantId);
160
+ for (const dropped of unique) {
161
+ if (!tenantScoped.find((row) => row.id === dropped)) {
162
+ console.warn(`[catalog.draft_description_from_media] dropping attachment not in scope: ${dropped}`);
163
+ }
164
+ }
165
+ return tenantScoped.map((row) => ({
166
+ attachmentId: row.id,
167
+ fileName: row.fileName,
168
+ mediaType: row.mimeType ?? null,
169
+ size: row.fileSize ?? null
170
+ }));
171
+ }
172
+ const draftDescriptionFromMediaTool = {
173
+ name: "catalog.draft_description_from_media",
174
+ displayName: "Draft description from media",
175
+ description: "Structured-output helper for D18. Returns the product bundle, the product's media metadata, and any tenant-scoped user-uploaded attachment IDs (cross-tenant IDs are dropped with a warning). The handler does NOT fetch attachment bytes \u2014 the Step 3.7 attachment bridge resolves bytes when the surrounding agent turn dispatches this tool. Requires a vision-capable provider to use raw image bytes; OCR-text fallback for non-vision providers already flows through the Step 3.7 bridge.",
176
+ inputSchema: draftDescriptionFromMediaInput,
177
+ requiredFeatures: ["catalog.products.view"],
178
+ tags: ["read", "catalog", "authoring", "merchandising", "media"],
179
+ isMutation: false,
180
+ supportsAttachments: true,
181
+ handler: async (rawInput, ctx) => {
182
+ const { tenantId } = assertTenantScope(ctx);
183
+ const input = draftDescriptionFromMediaInput.parse(rawInput);
184
+ const hit = await loadProductContext(ctx, tenantId, input.productId);
185
+ if (!hit.found) {
186
+ return { found: false, productId: input.productId };
187
+ }
188
+ const em = resolveEm(ctx);
189
+ const userMedia = await loadUserMedia(em, ctx, tenantId, input.userUploadedAttachmentIds ?? []);
190
+ const productMedia = hit.bundle.media.map((entry) => ({
191
+ attachmentId: entry.attachmentId,
192
+ fileName: entry.fileName,
193
+ mediaType: entry.mediaType,
194
+ altText: entry.altText,
195
+ sortOrder: entry.sortOrder
196
+ }));
197
+ return {
198
+ found: true,
199
+ proposal: {
200
+ description: "",
201
+ features: [],
202
+ mediaReferences: []
203
+ },
204
+ context: {
205
+ product: hit.bundle,
206
+ productMedia,
207
+ userMedia
208
+ },
209
+ outputSchemaDescriptor: {
210
+ schemaName: DRAFT_DESCRIPTION_FROM_MEDIA_SCHEMA,
211
+ jsonSchema: toJsonSchema(draftDescriptionFromMediaProposal, DRAFT_DESCRIPTION_FROM_MEDIA_SCHEMA)
212
+ }
213
+ };
214
+ }
215
+ };
216
+ const suggestTitleVariantsInput = z.object({
217
+ productId: z.string().uuid().describe("Catalog product id (UUID)."),
218
+ targetStyle: z.enum(["short", "seo", "marketplace"]).describe("Voice/style for the variants (short, seo-optimized, or marketplace-friendly)."),
219
+ maxVariants: z.number().int().min(1).max(5).optional().describe("Max number of variants to return. Default 3, hard-capped at 5.")
220
+ });
221
+ const suggestTitleVariantsProposal = z.object({
222
+ variants: z.array(
223
+ z.object({
224
+ title: z.string(),
225
+ style: z.string(),
226
+ rationale: z.string().optional()
227
+ })
228
+ ).describe("Proposed title variants (respect the `maxVariants` bound).")
229
+ });
230
+ const SUGGEST_TITLE_VARIANTS_SCHEMA = "SuggestTitleVariants";
231
+ const suggestTitleVariantsTool = {
232
+ name: "catalog.suggest_title_variants",
233
+ displayName: "Suggest title variants",
234
+ description: "Structured-output helper for D18: returns the product bundle and the target style. The surrounding agent turn uses `runAiAgentObject` to propose `maxVariants` (default 3, capped at 5) title variants matching the style.",
235
+ inputSchema: suggestTitleVariantsInput,
236
+ requiredFeatures: ["catalog.products.view"],
237
+ tags: ["read", "catalog", "authoring", "merchandising"],
238
+ isMutation: false,
239
+ handler: async (rawInput, ctx) => {
240
+ const { tenantId } = assertTenantScope(ctx);
241
+ const input = suggestTitleVariantsInput.parse(rawInput);
242
+ const hit = await loadProductContext(ctx, tenantId, input.productId);
243
+ if (!hit.found) {
244
+ return { found: false, productId: input.productId };
245
+ }
246
+ const maxVariants = Math.min(input.maxVariants ?? 3, 5);
247
+ return {
248
+ found: true,
249
+ proposal: {
250
+ variants: []
251
+ },
252
+ context: {
253
+ product: hit.bundle,
254
+ targetStyle: input.targetStyle,
255
+ maxVariants
256
+ },
257
+ outputSchemaDescriptor: {
258
+ schemaName: SUGGEST_TITLE_VARIANTS_SCHEMA,
259
+ jsonSchema: toJsonSchema(suggestTitleVariantsProposal, SUGGEST_TITLE_VARIANTS_SCHEMA)
260
+ }
261
+ };
262
+ }
263
+ };
264
+ const suggestPriceAdjustmentInput = z.object({
265
+ productId: z.string().uuid().describe("Catalog product id (UUID)."),
266
+ intent: z.string().trim().min(1).describe('Human-readable intent (e.g. "raise price by 10%", "match competitor at 29.99").'),
267
+ priceKindId: z.string().uuid().optional().describe("Optional price-kind id to target. When omitted, the model picks the most relevant kind from `context.availablePriceKinds`.")
268
+ });
269
+ const suggestPriceAdjustmentProposal = z.object({
270
+ currentPrice: z.union([
271
+ z.object({
272
+ amount: z.number(),
273
+ currency: z.string(),
274
+ priceKindId: z.string()
275
+ }),
276
+ z.null()
277
+ ]).describe("Current price on the targeted kind (null if none is defined)."),
278
+ proposedPrice: z.object({
279
+ amount: z.number(),
280
+ currency: z.string(),
281
+ priceKindId: z.string()
282
+ }),
283
+ rationale: z.string(),
284
+ constraints: z.object({
285
+ respectedPriceKindScope: z.boolean(),
286
+ respectedCurrency: z.boolean()
287
+ })
288
+ });
289
+ const SUGGEST_PRICE_ADJUSTMENT_SCHEMA = "SuggestPriceAdjustment";
290
+ function currentPriceFromBundle(bundle, priceKindId) {
291
+ const priceRows = bundle.prices.all;
292
+ if (!priceRows.length) return null;
293
+ const candidates = priceKindId ? priceRows.filter((row) => row.priceKindId === priceKindId) : priceRows;
294
+ const pick = candidates[0] ?? null;
295
+ if (!pick) return null;
296
+ const rawAmount = pick.unitPriceGross ?? pick.unitPriceNet;
297
+ const amount = rawAmount === null || rawAmount === void 0 ? null : Number(rawAmount);
298
+ if (amount === null || !Number.isFinite(amount)) return null;
299
+ const currency = pick.currencyCode ?? null;
300
+ const resolvedKind = pick.priceKindId ?? null;
301
+ if (!currency || !resolvedKind) return null;
302
+ return { amount, currency, priceKindId: resolvedKind };
303
+ }
304
+ async function currentPriceFromService(ctx, bundle, priceKindId) {
305
+ const pricingService = resolvePricingService(ctx);
306
+ if (!pricingService) return "resolver_unavailable";
307
+ const priceRows = bundle.prices.all;
308
+ if (!priceRows.length) return null;
309
+ const candidates = priceKindId ? priceRows.filter((row) => row.priceKindId === priceKindId) : priceRows;
310
+ if (!candidates.length) return null;
311
+ try {
312
+ const pricingContext = { quantity: 1, date: /* @__PURE__ */ new Date() };
313
+ const resolved = await pricingService.resolvePrice(
314
+ candidates,
315
+ pricingContext
316
+ );
317
+ if (!resolved) return null;
318
+ const rawAmount = resolved.unitPriceGross ?? resolved.unitPriceNet;
319
+ const amount = rawAmount === null || rawAmount === void 0 ? null : Number(rawAmount);
320
+ if (amount === null || !Number.isFinite(amount)) return null;
321
+ const currency = resolved.currencyCode ?? null;
322
+ const resolvedKind = resolved.priceKindId ?? (resolved.priceKind && typeof resolved.priceKind === "object" ? resolved.priceKind.id : resolved.priceKind);
323
+ if (!currency || !resolvedKind) return null;
324
+ return { amount, currency, priceKindId: resolvedKind };
325
+ } catch {
326
+ return "resolver_unavailable";
327
+ }
328
+ }
329
+ const suggestPriceAdjustmentTool = {
330
+ name: "catalog.suggest_price_adjustment",
331
+ displayName: "Suggest price adjustment",
332
+ description: "Structured-output helper for D18. Returns the product bundle, the current tenant-resolved price (via `catalogPricingService.selectBestPrice` when available, otherwise a fallback projection from the bundle prices), and the list of available price kinds. The surrounding agent turn uses `runAiAgentObject` to propose a price adjustment. `isMutation: false` is explicit \u2014 this tool NEVER writes; `catalog.update_product` (Step 5.14) recalculates authoritatively on the server before any write.",
333
+ inputSchema: suggestPriceAdjustmentInput,
334
+ requiredFeatures: ["catalog.pricing.manage"],
335
+ tags: ["read", "catalog", "authoring", "merchandising", "pricing"],
336
+ isMutation: false,
337
+ handler: async (rawInput, ctx) => {
338
+ const { tenantId } = assertTenantScope(ctx);
339
+ const input = suggestPriceAdjustmentInput.parse(rawInput);
340
+ const hit = await loadProductContext(ctx, tenantId, input.productId);
341
+ if (!hit.found) {
342
+ return { found: false, productId: input.productId };
343
+ }
344
+ const serviceResult = await currentPriceFromService(ctx, hit.bundle, input.priceKindId);
345
+ const currentPrice = serviceResult === "resolver_unavailable" ? currentPriceFromBundle(hit.bundle, input.priceKindId) : serviceResult;
346
+ const priceKindsResult = await listPriceKindsCore(ctx, { limit: 100 }, tenantId);
347
+ const availablePriceKinds = priceKindsResult.items.map((row) => ({
348
+ id: row.id,
349
+ code: row.code,
350
+ scope: row.organizationId ? "organization" : "tenant"
351
+ }));
352
+ return {
353
+ found: true,
354
+ proposal: {
355
+ currentPrice,
356
+ proposedPrice: {
357
+ amount: currentPrice?.amount ?? 0,
358
+ currency: currentPrice?.currency ?? (hit.bundle.product.primaryCurrencyCode ?? "USD"),
359
+ priceKindId: currentPrice?.priceKindId ?? (input.priceKindId ?? "")
360
+ },
361
+ rationale: "",
362
+ constraints: {
363
+ respectedPriceKindScope: true,
364
+ respectedCurrency: true
365
+ }
366
+ },
367
+ context: {
368
+ product: hit.bundle,
369
+ intent: input.intent,
370
+ availablePriceKinds
371
+ },
372
+ outputSchemaDescriptor: {
373
+ schemaName: SUGGEST_PRICE_ADJUSTMENT_SCHEMA,
374
+ jsonSchema: toJsonSchema(suggestPriceAdjustmentProposal, SUGGEST_PRICE_ADJUSTMENT_SCHEMA)
375
+ }
376
+ };
377
+ }
378
+ };
379
+ const authoringAiTools = [
380
+ draftDescriptionFromAttributesTool,
381
+ extractAttributesFromDescriptionTool,
382
+ draftDescriptionFromMediaTool,
383
+ suggestTitleVariantsTool,
384
+ suggestPriceAdjustmentTool
385
+ ];
386
+ var authoring_pack_default = authoringAiTools;
387
+ export {
388
+ authoringAiTools,
389
+ authoring_pack_default as default
390
+ };
391
+ //# sourceMappingURL=authoring-pack.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../../src/modules/catalog/ai-tools/authoring-pack.ts"],
4
+ "sourcesContent": ["/**\n * D18 catalog AI-authoring tools (Phase 1 WS-C, Step 3.12).\n *\n * Five structured-output helpers the `catalog.merchandising_assistant`\n * agent (Step 4.9) whitelists verbatim. These tools never write to the\n * database and never open a fresh model call from inside their handlers.\n * Instead, each handler assembles the tenant-scoped context the model will\n * need (product bundle, attribute schema, media references) and returns\n * an `{ proposal, context, outputSchemaDescriptor }` contract:\n *\n * - `context` \u2014 tenant-validated input for the model's\n * structured-output step.\n * - `outputSchemaDescriptor` \u2014 a `{ schemaName, jsonSchema }` descriptor\n * the agent runtime passes into\n * `runAiAgentObject` (Step 3.5) so the SAME\n * agent turn that invoked the tool fills the\n * `proposal` via structured output.\n * - `proposal` \u2014 a zod-typed placeholder with the fields the\n * model is expected to populate. Handlers\n * return empty defaults; the model fills them.\n *\n * This preserves the \"no database writes + no extra model round trip from\n * a tool\" contract from the brief. Any downstream mutation (e.g.\n * `catalog.update_product` in Step 5.14) re-validates the proposal\n * authoritatively on the server before writing.\n *\n * Five tools:\n * - `catalog.draft_description_from_attributes`\n * - `catalog.extract_attributes_from_description`\n * - `catalog.draft_description_from_media`\n * - `catalog.suggest_title_variants`\n * - `catalog.suggest_price_adjustment`\n *\n * Every tool sets `isMutation: false` explicitly (spec \u00A77 line 536 calls\n * this out specifically for `suggest_price_adjustment`; the whole authoring\n * pack mirrors the flag for consistency).\n *\n * Tenant scoping: all lookups route through the shared `_shared.ts`\n * helpers (`buildProductBundle`, `resolveAttributeSchema`,\n * `listPriceKindsCore`) and the shared attachment + product-price readers.\n * Never touches raw `em.find` / `em.findOne`.\n */\nimport type { EntityManager } from '@mikro-orm/postgresql'\nimport { z } from 'zod'\nimport { findWithDecryption } from '@open-mercato/shared/lib/encryption/find'\nimport { Attachment } from '@open-mercato/core/modules/attachments/data/entities'\nimport { E } from '#generated/entities.ids.generated'\nimport { assertTenantScope, type CatalogAiToolDefinition, type CatalogToolContext } from './types'\nimport {\n buildProductBundle,\n buildScope,\n listPriceKindsCore,\n resolveAttributeSchema,\n resolveEm,\n type AttributeSchemaResult,\n type ProductBundle,\n type ProductBundleResult,\n} from './_shared'\nimport type { CatalogPricingService } from '../services/catalogPricingService'\nimport type { PriceRow, PricingContext } from '../lib/pricing'\n\n/* -------------------------------------------------------------------------- */\n/* Shared helpers */\n/* -------------------------------------------------------------------------- */\n\nfunction toJsonSchema<T>(schema: z.ZodType<T>, schemaName: string): Record<string, unknown> {\n const json = z.toJSONSchema(schema) as Record<string, unknown>\n return { ...json, title: schemaName }\n}\n\ntype ProductContextHit = {\n found: true\n productId: string\n bundle: ProductBundle\n}\n\ntype ProductContextMiss = {\n found: false\n productId: string\n}\n\nasync function loadProductContext(\n ctx: CatalogToolContext,\n tenantId: string,\n productId: string,\n): Promise<ProductContextHit | ProductContextMiss> {\n const em = resolveEm(ctx)\n const result: ProductBundleResult = await buildProductBundle(em, ctx, tenantId, productId)\n if (!result.found) {\n return { found: false as const, productId }\n }\n return { found: true as const, productId, bundle: result }\n}\n\nfunction resolvePricingService(ctx: CatalogToolContext): CatalogPricingService | null {\n try {\n return ctx.container.resolve<CatalogPricingService>('catalogPricingService')\n } catch {\n return null\n }\n}\n\n/* -------------------------------------------------------------------------- */\n/* catalog.draft_description_from_attributes */\n/* -------------------------------------------------------------------------- */\n\nconst draftDescriptionFromAttributesInput = z.object({\n productId: z.string().uuid().describe('Catalog product id (UUID).'),\n tonePreference: z\n .enum(['neutral', 'marketing', 'technical', 'short'])\n .optional()\n .describe('Preferred tone for the drafted description (default `neutral`).'),\n})\n\nconst draftDescriptionFromAttributesProposal = z.object({\n description: z.string().describe('Drafted product description the model should author from the attribute bundle.'),\n rationale: z.string().describe('Why the model authored this description (which attributes drove it).'),\n attributesUsed: z.array(z.string()).describe('Attribute keys the model actually referenced.'),\n})\n\nconst DRAFT_DESCRIPTION_FROM_ATTRIBUTES_SCHEMA = 'DraftDescriptionFromAttributes'\n\nconst draftDescriptionFromAttributesTool: CatalogAiToolDefinition = {\n name: 'catalog.draft_description_from_attributes',\n displayName: 'Draft description from attributes',\n description:\n 'Structured-output helper for D18: returns the tenant-scoped product bundle (core fields + attributes + media metadata) alongside a JSON-Schema descriptor the surrounding agent turn uses via `runAiAgentObject` to draft a product description. The handler NEVER calls the model itself; it composes the context and the output shape.',\n inputSchema: draftDescriptionFromAttributesInput,\n requiredFeatures: ['catalog.products.view'],\n tags: ['read', 'catalog', 'authoring', 'merchandising'],\n isMutation: false,\n handler: async (rawInput, ctx) => {\n const { tenantId } = assertTenantScope(ctx)\n const input = draftDescriptionFromAttributesInput.parse(rawInput)\n const hit = await loadProductContext(ctx, tenantId, input.productId)\n if (!hit.found) {\n return { found: false as const, productId: input.productId }\n }\n const tonePreference = input.tonePreference ?? 'neutral'\n return {\n found: true as const,\n proposal: {\n description: '',\n rationale: '',\n attributesUsed: [] as string[],\n },\n context: {\n product: hit.bundle,\n tonePreference,\n },\n outputSchemaDescriptor: {\n schemaName: DRAFT_DESCRIPTION_FROM_ATTRIBUTES_SCHEMA,\n jsonSchema: toJsonSchema(draftDescriptionFromAttributesProposal, DRAFT_DESCRIPTION_FROM_ATTRIBUTES_SCHEMA),\n },\n }\n },\n}\n\n/* -------------------------------------------------------------------------- */\n/* catalog.extract_attributes_from_description */\n/* -------------------------------------------------------------------------- */\n\nconst extractAttributesFromDescriptionInput = z.object({\n productId: z.string().uuid().describe('Catalog product id (UUID).'),\n descriptionOverride: z\n .string()\n .min(1)\n .optional()\n .describe('Optional description text to analyze instead of the product\\'s stored description.'),\n})\n\n// `attributes` is modeled as a free-form `Record<string, unknown>` because\n// the attribute schema is tenant-defined and heterogeneous (enum / numeric /\n// boolean / string-with-unit). `z.record` emits JSON Schema\n// `additionalProperties: {}` which is the intended open-object surface for\n// the model; downstream validation (Step 5.14 `apply_attribute_extraction`)\n// re-checks each value against the resolved schema authoritatively.\nconst extractAttributesFromDescriptionProposal = z.object({\n attributes: z\n .record(z.string(), z.unknown())\n .describe('Attribute key \u2192 value pairs mapped from the description. Shape matches the resolved attribute schema; server re-validates before any write.'),\n confidence: z\n .number()\n .min(0)\n .max(1)\n .describe('Model confidence (0..1) in the extracted attribute set as a whole.'),\n unmapped: z\n .array(z.string())\n .describe('Description phrases that could not be mapped to any schema attribute.'),\n})\n\nconst EXTRACT_ATTRIBUTES_FROM_DESCRIPTION_SCHEMA = 'ExtractAttributesFromDescription'\n\nconst extractAttributesFromDescriptionTool: CatalogAiToolDefinition = {\n name: 'catalog.extract_attributes_from_description',\n displayName: 'Extract attributes from description',\n description:\n 'Structured-output helper for D18: returns the product bundle, the resolved attribute schema, and the description text the model should parse. The surrounding agent turn uses `runAiAgentObject` with the emitted JSON-Schema descriptor to produce the extracted attribute map. The `attributes` output shape intentionally uses `additionalProperties: true` because tenant attribute schemas are heterogeneous; any downstream write re-validates each value against the schema authoritatively.',\n inputSchema: extractAttributesFromDescriptionInput,\n requiredFeatures: ['catalog.products.view'],\n tags: ['read', 'catalog', 'authoring', 'merchandising'],\n isMutation: false,\n handler: async (rawInput, ctx) => {\n const { tenantId } = assertTenantScope(ctx)\n const input = extractAttributesFromDescriptionInput.parse(rawInput)\n const hit = await loadProductContext(ctx, tenantId, input.productId)\n if (!hit.found) {\n return { found: false as const, productId: input.productId }\n }\n const attributeSchema: AttributeSchemaResult = await resolveAttributeSchema(\n ctx,\n tenantId,\n input.productId,\n undefined,\n )\n const description = input.descriptionOverride ?? hit.bundle.product.description ?? ''\n return {\n found: true as const,\n proposal: {\n attributes: {} as Record<string, unknown>,\n confidence: 0,\n unmapped: [] as string[],\n },\n context: {\n product: hit.bundle,\n attributeSchema,\n description,\n },\n outputSchemaDescriptor: {\n schemaName: EXTRACT_ATTRIBUTES_FROM_DESCRIPTION_SCHEMA,\n jsonSchema: toJsonSchema(\n extractAttributesFromDescriptionProposal,\n EXTRACT_ATTRIBUTES_FROM_DESCRIPTION_SCHEMA,\n ),\n },\n }\n },\n}\n\n/* -------------------------------------------------------------------------- */\n/* catalog.draft_description_from_media */\n/* -------------------------------------------------------------------------- */\n\nconst draftDescriptionFromMediaInput = z.object({\n productId: z.string().uuid().describe('Catalog product id (UUID).'),\n userUploadedAttachmentIds: z\n .array(z.string().uuid())\n .max(20)\n .optional()\n .describe('Additional attachment IDs the user just uploaded. Cross-tenant IDs are dropped with a warning (not surfaced).'),\n})\n\nconst draftDescriptionFromMediaProposal = z.object({\n description: z.string().describe('Drafted description based on the media references.'),\n features: z.array(z.string()).describe('Salient product features the model surfaced from the media.'),\n mediaReferences: z\n .array(\n z.object({\n attachmentId: z.string(),\n note: z.string().optional(),\n }),\n )\n .describe('Attachment IDs the model actually used, with an optional note per reference.'),\n})\n\nconst DRAFT_DESCRIPTION_FROM_MEDIA_SCHEMA = 'DraftDescriptionFromMedia'\n\ntype UserMediaEntry = {\n attachmentId: string\n fileName: string\n mediaType: string | null\n size: number | null\n}\n\nasync function loadUserMedia(\n em: EntityManager,\n ctx: CatalogToolContext,\n tenantId: string,\n attachmentIds: string[],\n): Promise<UserMediaEntry[]> {\n if (!attachmentIds.length) return []\n const unique = Array.from(new Set(attachmentIds))\n const where: Record<string, unknown> = {\n tenantId,\n id: { $in: unique },\n }\n if (ctx.organizationId) where.organizationId = ctx.organizationId\n const rows = await findWithDecryption<Attachment>(\n em,\n Attachment,\n where as any,\n { limit: unique.length } as any,\n buildScope(ctx, tenantId),\n )\n const tenantScoped = rows.filter((row) => (row.tenantId ?? null) === tenantId)\n for (const dropped of unique) {\n if (!tenantScoped.find((row) => row.id === dropped)) {\n console.warn(`[catalog.draft_description_from_media] dropping attachment not in scope: ${dropped}`)\n }\n }\n return tenantScoped.map((row) => ({\n attachmentId: row.id,\n fileName: row.fileName,\n mediaType: row.mimeType ?? null,\n size: row.fileSize ?? null,\n }))\n}\n\nconst draftDescriptionFromMediaTool: CatalogAiToolDefinition = {\n name: 'catalog.draft_description_from_media',\n displayName: 'Draft description from media',\n description:\n 'Structured-output helper for D18. Returns the product bundle, the product\\'s media metadata, and any tenant-scoped user-uploaded attachment IDs (cross-tenant IDs are dropped with a warning). The handler does NOT fetch attachment bytes \u2014 the Step 3.7 attachment bridge resolves bytes when the surrounding agent turn dispatches this tool. Requires a vision-capable provider to use raw image bytes; OCR-text fallback for non-vision providers already flows through the Step 3.7 bridge.',\n inputSchema: draftDescriptionFromMediaInput,\n requiredFeatures: ['catalog.products.view'],\n tags: ['read', 'catalog', 'authoring', 'merchandising', 'media'],\n isMutation: false,\n supportsAttachments: true,\n handler: async (rawInput, ctx) => {\n const { tenantId } = assertTenantScope(ctx)\n const input = draftDescriptionFromMediaInput.parse(rawInput)\n const hit = await loadProductContext(ctx, tenantId, input.productId)\n if (!hit.found) {\n return { found: false as const, productId: input.productId }\n }\n const em = resolveEm(ctx)\n const userMedia = await loadUserMedia(em, ctx, tenantId, input.userUploadedAttachmentIds ?? [])\n const productMedia = hit.bundle.media.map((entry) => ({\n attachmentId: entry.attachmentId,\n fileName: entry.fileName,\n mediaType: entry.mediaType,\n altText: entry.altText,\n sortOrder: entry.sortOrder,\n }))\n return {\n found: true as const,\n proposal: {\n description: '',\n features: [] as string[],\n mediaReferences: [] as Array<{ attachmentId: string; note?: string }>,\n },\n context: {\n product: hit.bundle,\n productMedia,\n userMedia,\n },\n outputSchemaDescriptor: {\n schemaName: DRAFT_DESCRIPTION_FROM_MEDIA_SCHEMA,\n jsonSchema: toJsonSchema(draftDescriptionFromMediaProposal, DRAFT_DESCRIPTION_FROM_MEDIA_SCHEMA),\n },\n }\n },\n}\n\n/* -------------------------------------------------------------------------- */\n/* catalog.suggest_title_variants */\n/* -------------------------------------------------------------------------- */\n\nconst suggestTitleVariantsInput = z.object({\n productId: z.string().uuid().describe('Catalog product id (UUID).'),\n targetStyle: z\n .enum(['short', 'seo', 'marketplace'])\n .describe('Voice/style for the variants (short, seo-optimized, or marketplace-friendly).'),\n maxVariants: z\n .number()\n .int()\n .min(1)\n .max(5)\n .optional()\n .describe('Max number of variants to return. Default 3, hard-capped at 5.'),\n})\n\nconst suggestTitleVariantsProposal = z.object({\n variants: z\n .array(\n z.object({\n title: z.string(),\n style: z.string(),\n rationale: z.string().optional(),\n }),\n )\n .describe('Proposed title variants (respect the `maxVariants` bound).'),\n})\n\nconst SUGGEST_TITLE_VARIANTS_SCHEMA = 'SuggestTitleVariants'\n\nconst suggestTitleVariantsTool: CatalogAiToolDefinition = {\n name: 'catalog.suggest_title_variants',\n displayName: 'Suggest title variants',\n description:\n 'Structured-output helper for D18: returns the product bundle and the target style. The surrounding agent turn uses `runAiAgentObject` to propose `maxVariants` (default 3, capped at 5) title variants matching the style.',\n inputSchema: suggestTitleVariantsInput,\n requiredFeatures: ['catalog.products.view'],\n tags: ['read', 'catalog', 'authoring', 'merchandising'],\n isMutation: false,\n handler: async (rawInput, ctx) => {\n const { tenantId } = assertTenantScope(ctx)\n const input = suggestTitleVariantsInput.parse(rawInput)\n const hit = await loadProductContext(ctx, tenantId, input.productId)\n if (!hit.found) {\n return { found: false as const, productId: input.productId }\n }\n const maxVariants = Math.min(input.maxVariants ?? 3, 5)\n return {\n found: true as const,\n proposal: {\n variants: [] as Array<{ title: string; style: string; rationale?: string }>,\n },\n context: {\n product: hit.bundle,\n targetStyle: input.targetStyle,\n maxVariants,\n },\n outputSchemaDescriptor: {\n schemaName: SUGGEST_TITLE_VARIANTS_SCHEMA,\n jsonSchema: toJsonSchema(suggestTitleVariantsProposal, SUGGEST_TITLE_VARIANTS_SCHEMA),\n },\n }\n },\n}\n\n/* -------------------------------------------------------------------------- */\n/* catalog.suggest_price_adjustment */\n/* -------------------------------------------------------------------------- */\n\nconst suggestPriceAdjustmentInput = z.object({\n productId: z.string().uuid().describe('Catalog product id (UUID).'),\n intent: z\n .string()\n .trim()\n .min(1)\n .describe('Human-readable intent (e.g. \"raise price by 10%\", \"match competitor at 29.99\").'),\n priceKindId: z\n .string()\n .uuid()\n .optional()\n .describe('Optional price-kind id to target. When omitted, the model picks the most relevant kind from `context.availablePriceKinds`.'),\n})\n\nconst suggestPriceAdjustmentProposal = z.object({\n currentPrice: z\n .union([\n z.object({\n amount: z.number(),\n currency: z.string(),\n priceKindId: z.string(),\n }),\n z.null(),\n ])\n .describe('Current price on the targeted kind (null if none is defined).'),\n proposedPrice: z.object({\n amount: z.number(),\n currency: z.string(),\n priceKindId: z.string(),\n }),\n rationale: z.string(),\n constraints: z.object({\n respectedPriceKindScope: z.boolean(),\n respectedCurrency: z.boolean(),\n }),\n})\n\nconst SUGGEST_PRICE_ADJUSTMENT_SCHEMA = 'SuggestPriceAdjustment'\n\ntype ResolvedCurrentPrice = {\n amount: number\n currency: string\n priceKindId: string\n} | null\n\nfunction currentPriceFromBundle(\n bundle: ProductBundle,\n priceKindId?: string,\n): ResolvedCurrentPrice {\n const priceRows = bundle.prices.all as Array<Record<string, unknown>>\n if (!priceRows.length) return null\n const candidates = priceKindId\n ? priceRows.filter((row) => row.priceKindId === priceKindId)\n : priceRows\n const pick = candidates[0] ?? null\n if (!pick) return null\n const rawAmount = (pick.unitPriceGross ?? pick.unitPriceNet) as string | null | undefined\n const amount = rawAmount === null || rawAmount === undefined ? null : Number(rawAmount)\n if (amount === null || !Number.isFinite(amount)) return null\n const currency = (pick.currencyCode as string | null) ?? null\n const resolvedKind = (pick.priceKindId as string | null) ?? null\n if (!currency || !resolvedKind) return null\n return { amount, currency, priceKindId: resolvedKind }\n}\n\nasync function currentPriceFromService(\n ctx: CatalogToolContext,\n bundle: ProductBundle,\n priceKindId?: string,\n): Promise<ResolvedCurrentPrice | 'resolver_unavailable'> {\n const pricingService = resolvePricingService(ctx)\n if (!pricingService) return 'resolver_unavailable'\n const priceRows = bundle.prices.all as Array<Record<string, unknown>>\n if (!priceRows.length) return null\n const candidates = priceKindId\n ? priceRows.filter((row) => row.priceKindId === priceKindId)\n : priceRows\n if (!candidates.length) return null\n try {\n const pricingContext: PricingContext = { quantity: 1, date: new Date() }\n const resolved = await pricingService.resolvePrice(\n candidates as unknown as PriceRow[],\n pricingContext,\n )\n if (!resolved) return null\n const rawAmount = ((resolved as any).unitPriceGross ?? (resolved as any).unitPriceNet) as\n | string\n | number\n | null\n | undefined\n const amount = rawAmount === null || rawAmount === undefined ? null : Number(rawAmount)\n if (amount === null || !Number.isFinite(amount)) return null\n const currency = ((resolved as any).currencyCode as string | null) ?? null\n const resolvedKind = ((resolved as any).priceKindId\n ?? ((resolved as any).priceKind && typeof (resolved as any).priceKind === 'object'\n ? (resolved as any).priceKind.id\n : (resolved as any).priceKind)) as string | null | undefined\n if (!currency || !resolvedKind) return null\n return { amount, currency, priceKindId: resolvedKind }\n } catch {\n // The pricing service may throw for reasons beyond our control (config,\n // resolver chain error). We fall back to the bundle view rather than\n // bubbling the error out of a read-only tool.\n return 'resolver_unavailable'\n }\n}\n\nconst suggestPriceAdjustmentTool: CatalogAiToolDefinition = {\n name: 'catalog.suggest_price_adjustment',\n displayName: 'Suggest price adjustment',\n description:\n 'Structured-output helper for D18. Returns the product bundle, the current tenant-resolved price (via `catalogPricingService.selectBestPrice` when available, otherwise a fallback projection from the bundle prices), and the list of available price kinds. The surrounding agent turn uses `runAiAgentObject` to propose a price adjustment. `isMutation: false` is explicit \u2014 this tool NEVER writes; `catalog.update_product` (Step 5.14) recalculates authoritatively on the server before any write.',\n inputSchema: suggestPriceAdjustmentInput,\n requiredFeatures: ['catalog.pricing.manage'],\n tags: ['read', 'catalog', 'authoring', 'merchandising', 'pricing'],\n isMutation: false,\n handler: async (rawInput, ctx) => {\n const { tenantId } = assertTenantScope(ctx)\n const input = suggestPriceAdjustmentInput.parse(rawInput)\n const hit = await loadProductContext(ctx, tenantId, input.productId)\n if (!hit.found) {\n return { found: false as const, productId: input.productId }\n }\n const serviceResult = await currentPriceFromService(ctx, hit.bundle, input.priceKindId)\n const currentPrice: ResolvedCurrentPrice =\n serviceResult === 'resolver_unavailable'\n ? currentPriceFromBundle(hit.bundle, input.priceKindId)\n : serviceResult\n const priceKindsResult = await listPriceKindsCore(ctx, { limit: 100 }, tenantId)\n const availablePriceKinds = priceKindsResult.items.map((row) => ({\n id: row.id,\n code: row.code,\n scope: row.organizationId ? ('organization' as const) : ('tenant' as const),\n }))\n return {\n found: true as const,\n proposal: {\n currentPrice,\n proposedPrice: {\n amount: currentPrice?.amount ?? 0,\n currency: currentPrice?.currency ?? (hit.bundle.product.primaryCurrencyCode ?? 'USD'),\n priceKindId: currentPrice?.priceKindId ?? (input.priceKindId ?? ''),\n },\n rationale: '',\n constraints: {\n respectedPriceKindScope: true,\n respectedCurrency: true,\n },\n },\n context: {\n product: hit.bundle,\n intent: input.intent,\n availablePriceKinds,\n },\n outputSchemaDescriptor: {\n schemaName: SUGGEST_PRICE_ADJUSTMENT_SCHEMA,\n jsonSchema: toJsonSchema(suggestPriceAdjustmentProposal, SUGGEST_PRICE_ADJUSTMENT_SCHEMA),\n },\n }\n },\n}\n\n/* -------------------------------------------------------------------------- */\n/* Export */\n/* -------------------------------------------------------------------------- */\n\nexport const authoringAiTools: CatalogAiToolDefinition[] = [\n draftDescriptionFromAttributesTool,\n extractAttributesFromDescriptionTool,\n draftDescriptionFromMediaTool,\n suggestTitleVariantsTool,\n suggestPriceAdjustmentTool,\n]\n\nexport default authoringAiTools\n"],
5
+ "mappings": "AA2CA,SAAS,SAAS;AAClB,SAAS,0BAA0B;AACnC,SAAS,kBAAkB;AAE3B,SAAS,yBAAgF;AACzF;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OAIK;AAQP,SAAS,aAAgB,QAAsB,YAA6C;AAC1F,QAAM,OAAO,EAAE,aAAa,MAAM;AAClC,SAAO,EAAE,GAAG,MAAM,OAAO,WAAW;AACtC;AAaA,eAAe,mBACb,KACA,UACA,WACiD;AACjD,QAAM,KAAK,UAAU,GAAG;AACxB,QAAM,SAA8B,MAAM,mBAAmB,IAAI,KAAK,UAAU,SAAS;AACzF,MAAI,CAAC,OAAO,OAAO;AACjB,WAAO,EAAE,OAAO,OAAgB,UAAU;AAAA,EAC5C;AACA,SAAO,EAAE,OAAO,MAAe,WAAW,QAAQ,OAAO;AAC3D;AAEA,SAAS,sBAAsB,KAAuD;AACpF,MAAI;AACF,WAAO,IAAI,UAAU,QAA+B,uBAAuB;AAAA,EAC7E,QAAQ;AACN,WAAO;AAAA,EACT;AACF;AAMA,MAAM,sCAAsC,EAAE,OAAO;AAAA,EACnD,WAAW,EAAE,OAAO,EAAE,KAAK,EAAE,SAAS,4BAA4B;AAAA,EAClE,gBAAgB,EACb,KAAK,CAAC,WAAW,aAAa,aAAa,OAAO,CAAC,EACnD,SAAS,EACT,SAAS,iEAAiE;AAC/E,CAAC;AAED,MAAM,yCAAyC,EAAE,OAAO;AAAA,EACtD,aAAa,EAAE,OAAO,EAAE,SAAS,gFAAgF;AAAA,EACjH,WAAW,EAAE,OAAO,EAAE,SAAS,sEAAsE;AAAA,EACrG,gBAAgB,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,SAAS,+CAA+C;AAC9F,CAAC;AAED,MAAM,2CAA2C;AAEjD,MAAM,qCAA8D;AAAA,EAClE,MAAM;AAAA,EACN,aAAa;AAAA,EACb,aACE;AAAA,EACF,aAAa;AAAA,EACb,kBAAkB,CAAC,uBAAuB;AAAA,EAC1C,MAAM,CAAC,QAAQ,WAAW,aAAa,eAAe;AAAA,EACtD,YAAY;AAAA,EACZ,SAAS,OAAO,UAAU,QAAQ;AAChC,UAAM,EAAE,SAAS,IAAI,kBAAkB,GAAG;AAC1C,UAAM,QAAQ,oCAAoC,MAAM,QAAQ;AAChE,UAAM,MAAM,MAAM,mBAAmB,KAAK,UAAU,MAAM,SAAS;AACnE,QAAI,CAAC,IAAI,OAAO;AACd,aAAO,EAAE,OAAO,OAAgB,WAAW,MAAM,UAAU;AAAA,IAC7D;AACA,UAAM,iBAAiB,MAAM,kBAAkB;AAC/C,WAAO;AAAA,MACL,OAAO;AAAA,MACP,UAAU;AAAA,QACR,aAAa;AAAA,QACb,WAAW;AAAA,QACX,gBAAgB,CAAC;AAAA,MACnB;AAAA,MACA,SAAS;AAAA,QACP,SAAS,IAAI;AAAA,QACb;AAAA,MACF;AAAA,MACA,wBAAwB;AAAA,QACtB,YAAY;AAAA,QACZ,YAAY,aAAa,wCAAwC,wCAAwC;AAAA,MAC3G;AAAA,IACF;AAAA,EACF;AACF;AAMA,MAAM,wCAAwC,EAAE,OAAO;AAAA,EACrD,WAAW,EAAE,OAAO,EAAE,KAAK,EAAE,SAAS,4BAA4B;AAAA,EAClE,qBAAqB,EAClB,OAAO,EACP,IAAI,CAAC,EACL,SAAS,EACT,SAAS,mFAAoF;AAClG,CAAC;AAQD,MAAM,2CAA2C,EAAE,OAAO;AAAA,EACxD,YAAY,EACT,OAAO,EAAE,OAAO,GAAG,EAAE,QAAQ,CAAC,EAC9B,SAAS,kJAA6I;AAAA,EACzJ,YAAY,EACT,OAAO,EACP,IAAI,CAAC,EACL,IAAI,CAAC,EACL,SAAS,oEAAoE;AAAA,EAChF,UAAU,EACP,MAAM,EAAE,OAAO,CAAC,EAChB,SAAS,uEAAuE;AACrF,CAAC;AAED,MAAM,6CAA6C;AAEnD,MAAM,uCAAgE;AAAA,EACpE,MAAM;AAAA,EACN,aAAa;AAAA,EACb,aACE;AAAA,EACF,aAAa;AAAA,EACb,kBAAkB,CAAC,uBAAuB;AAAA,EAC1C,MAAM,CAAC,QAAQ,WAAW,aAAa,eAAe;AAAA,EACtD,YAAY;AAAA,EACZ,SAAS,OAAO,UAAU,QAAQ;AAChC,UAAM,EAAE,SAAS,IAAI,kBAAkB,GAAG;AAC1C,UAAM,QAAQ,sCAAsC,MAAM,QAAQ;AAClE,UAAM,MAAM,MAAM,mBAAmB,KAAK,UAAU,MAAM,SAAS;AACnE,QAAI,CAAC,IAAI,OAAO;AACd,aAAO,EAAE,OAAO,OAAgB,WAAW,MAAM,UAAU;AAAA,IAC7D;AACA,UAAM,kBAAyC,MAAM;AAAA,MACnD;AAAA,MACA;AAAA,MACA,MAAM;AAAA,MACN;AAAA,IACF;AACA,UAAM,cAAc,MAAM,uBAAuB,IAAI,OAAO,QAAQ,eAAe;AACnF,WAAO;AAAA,MACL,OAAO;AAAA,MACP,UAAU;AAAA,QACR,YAAY,CAAC;AAAA,QACb,YAAY;AAAA,QACZ,UAAU,CAAC;AAAA,MACb;AAAA,MACA,SAAS;AAAA,QACP,SAAS,IAAI;AAAA,QACb;AAAA,QACA;AAAA,MACF;AAAA,MACA,wBAAwB;AAAA,QACtB,YAAY;AAAA,QACZ,YAAY;AAAA,UACV;AAAA,UACA;AAAA,QACF;AAAA,MACF;AAAA,IACF;AAAA,EACF;AACF;AAMA,MAAM,iCAAiC,EAAE,OAAO;AAAA,EAC9C,WAAW,EAAE,OAAO,EAAE,KAAK,EAAE,SAAS,4BAA4B;AAAA,EAClE,2BAA2B,EACxB,MAAM,EAAE,OAAO,EAAE,KAAK,CAAC,EACvB,IAAI,EAAE,EACN,SAAS,EACT,SAAS,+GAA+G;AAC7H,CAAC;AAED,MAAM,oCAAoC,EAAE,OAAO;AAAA,EACjD,aAAa,EAAE,OAAO,EAAE,SAAS,oDAAoD;AAAA,EACrF,UAAU,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,SAAS,6DAA6D;AAAA,EACpG,iBAAiB,EACd;AAAA,IACC,EAAE,OAAO;AAAA,MACP,cAAc,EAAE,OAAO;AAAA,MACvB,MAAM,EAAE,OAAO,EAAE,SAAS;AAAA,IAC5B,CAAC;AAAA,EACH,EACC,SAAS,8EAA8E;AAC5F,CAAC;AAED,MAAM,sCAAsC;AAS5C,eAAe,cACb,IACA,KACA,UACA,eAC2B;AAC3B,MAAI,CAAC,cAAc,OAAQ,QAAO,CAAC;AACnC,QAAM,SAAS,MAAM,KAAK,IAAI,IAAI,aAAa,CAAC;AAChD,QAAM,QAAiC;AAAA,IACrC;AAAA,IACA,IAAI,EAAE,KAAK,OAAO;AAAA,EACpB;AACA,MAAI,IAAI,eAAgB,OAAM,iBAAiB,IAAI;AACnD,QAAM,OAAO,MAAM;AAAA,IACjB;AAAA,IACA;AAAA,IACA;AAAA,IACA,EAAE,OAAO,OAAO,OAAO;AAAA,IACvB,WAAW,KAAK,QAAQ;AAAA,EAC1B;AACA,QAAM,eAAe,KAAK,OAAO,CAAC,SAAS,IAAI,YAAY,UAAU,QAAQ;AAC7E,aAAW,WAAW,QAAQ;AAC5B,QAAI,CAAC,aAAa,KAAK,CAAC,QAAQ,IAAI,OAAO,OAAO,GAAG;AACnD,cAAQ,KAAK,4EAA4E,OAAO,EAAE;AAAA,IACpG;AAAA,EACF;AACA,SAAO,aAAa,IAAI,CAAC,SAAS;AAAA,IAChC,cAAc,IAAI;AAAA,IAClB,UAAU,IAAI;AAAA,IACd,WAAW,IAAI,YAAY;AAAA,IAC3B,MAAM,IAAI,YAAY;AAAA,EACxB,EAAE;AACJ;AAEA,MAAM,gCAAyD;AAAA,EAC7D,MAAM;AAAA,EACN,aAAa;AAAA,EACb,aACE;AAAA,EACF,aAAa;AAAA,EACb,kBAAkB,CAAC,uBAAuB;AAAA,EAC1C,MAAM,CAAC,QAAQ,WAAW,aAAa,iBAAiB,OAAO;AAAA,EAC/D,YAAY;AAAA,EACZ,qBAAqB;AAAA,EACrB,SAAS,OAAO,UAAU,QAAQ;AAChC,UAAM,EAAE,SAAS,IAAI,kBAAkB,GAAG;AAC1C,UAAM,QAAQ,+BAA+B,MAAM,QAAQ;AAC3D,UAAM,MAAM,MAAM,mBAAmB,KAAK,UAAU,MAAM,SAAS;AACnE,QAAI,CAAC,IAAI,OAAO;AACd,aAAO,EAAE,OAAO,OAAgB,WAAW,MAAM,UAAU;AAAA,IAC7D;AACA,UAAM,KAAK,UAAU,GAAG;AACxB,UAAM,YAAY,MAAM,cAAc,IAAI,KAAK,UAAU,MAAM,6BAA6B,CAAC,CAAC;AAC9F,UAAM,eAAe,IAAI,OAAO,MAAM,IAAI,CAAC,WAAW;AAAA,MACpD,cAAc,MAAM;AAAA,MACpB,UAAU,MAAM;AAAA,MAChB,WAAW,MAAM;AAAA,MACjB,SAAS,MAAM;AAAA,MACf,WAAW,MAAM;AAAA,IACnB,EAAE;AACF,WAAO;AAAA,MACL,OAAO;AAAA,MACP,UAAU;AAAA,QACR,aAAa;AAAA,QACb,UAAU,CAAC;AAAA,QACX,iBAAiB,CAAC;AAAA,MACpB;AAAA,MACA,SAAS;AAAA,QACP,SAAS,IAAI;AAAA,QACb;AAAA,QACA;AAAA,MACF;AAAA,MACA,wBAAwB;AAAA,QACtB,YAAY;AAAA,QACZ,YAAY,aAAa,mCAAmC,mCAAmC;AAAA,MACjG;AAAA,IACF;AAAA,EACF;AACF;AAMA,MAAM,4BAA4B,EAAE,OAAO;AAAA,EACzC,WAAW,EAAE,OAAO,EAAE,KAAK,EAAE,SAAS,4BAA4B;AAAA,EAClE,aAAa,EACV,KAAK,CAAC,SAAS,OAAO,aAAa,CAAC,EACpC,SAAS,+EAA+E;AAAA,EAC3F,aAAa,EACV,OAAO,EACP,IAAI,EACJ,IAAI,CAAC,EACL,IAAI,CAAC,EACL,SAAS,EACT,SAAS,gEAAgE;AAC9E,CAAC;AAED,MAAM,+BAA+B,EAAE,OAAO;AAAA,EAC5C,UAAU,EACP;AAAA,IACC,EAAE,OAAO;AAAA,MACP,OAAO,EAAE,OAAO;AAAA,MAChB,OAAO,EAAE,OAAO;AAAA,MAChB,WAAW,EAAE,OAAO,EAAE,SAAS;AAAA,IACjC,CAAC;AAAA,EACH,EACC,SAAS,4DAA4D;AAC1E,CAAC;AAED,MAAM,gCAAgC;AAEtC,MAAM,2BAAoD;AAAA,EACxD,MAAM;AAAA,EACN,aAAa;AAAA,EACb,aACE;AAAA,EACF,aAAa;AAAA,EACb,kBAAkB,CAAC,uBAAuB;AAAA,EAC1C,MAAM,CAAC,QAAQ,WAAW,aAAa,eAAe;AAAA,EACtD,YAAY;AAAA,EACZ,SAAS,OAAO,UAAU,QAAQ;AAChC,UAAM,EAAE,SAAS,IAAI,kBAAkB,GAAG;AAC1C,UAAM,QAAQ,0BAA0B,MAAM,QAAQ;AACtD,UAAM,MAAM,MAAM,mBAAmB,KAAK,UAAU,MAAM,SAAS;AACnE,QAAI,CAAC,IAAI,OAAO;AACd,aAAO,EAAE,OAAO,OAAgB,WAAW,MAAM,UAAU;AAAA,IAC7D;AACA,UAAM,cAAc,KAAK,IAAI,MAAM,eAAe,GAAG,CAAC;AACtD,WAAO;AAAA,MACL,OAAO;AAAA,MACP,UAAU;AAAA,QACR,UAAU,CAAC;AAAA,MACb;AAAA,MACA,SAAS;AAAA,QACP,SAAS,IAAI;AAAA,QACb,aAAa,MAAM;AAAA,QACnB;AAAA,MACF;AAAA,MACA,wBAAwB;AAAA,QACtB,YAAY;AAAA,QACZ,YAAY,aAAa,8BAA8B,6BAA6B;AAAA,MACtF;AAAA,IACF;AAAA,EACF;AACF;AAMA,MAAM,8BAA8B,EAAE,OAAO;AAAA,EAC3C,WAAW,EAAE,OAAO,EAAE,KAAK,EAAE,SAAS,4BAA4B;AAAA,EAClE,QAAQ,EACL,OAAO,EACP,KAAK,EACL,IAAI,CAAC,EACL,SAAS,iFAAiF;AAAA,EAC7F,aAAa,EACV,OAAO,EACP,KAAK,EACL,SAAS,EACT,SAAS,4HAA4H;AAC1I,CAAC;AAED,MAAM,iCAAiC,EAAE,OAAO;AAAA,EAC9C,cAAc,EACX,MAAM;AAAA,IACL,EAAE,OAAO;AAAA,MACP,QAAQ,EAAE,OAAO;AAAA,MACjB,UAAU,EAAE,OAAO;AAAA,MACnB,aAAa,EAAE,OAAO;AAAA,IACxB,CAAC;AAAA,IACD,EAAE,KAAK;AAAA,EACT,CAAC,EACA,SAAS,+DAA+D;AAAA,EAC3E,eAAe,EAAE,OAAO;AAAA,IACtB,QAAQ,EAAE,OAAO;AAAA,IACjB,UAAU,EAAE,OAAO;AAAA,IACnB,aAAa,EAAE,OAAO;AAAA,EACxB,CAAC;AAAA,EACD,WAAW,EAAE,OAAO;AAAA,EACpB,aAAa,EAAE,OAAO;AAAA,IACpB,yBAAyB,EAAE,QAAQ;AAAA,IACnC,mBAAmB,EAAE,QAAQ;AAAA,EAC/B,CAAC;AACH,CAAC;AAED,MAAM,kCAAkC;AAQxC,SAAS,uBACP,QACA,aACsB;AACtB,QAAM,YAAY,OAAO,OAAO;AAChC,MAAI,CAAC,UAAU,OAAQ,QAAO;AAC9B,QAAM,aAAa,cACf,UAAU,OAAO,CAAC,QAAQ,IAAI,gBAAgB,WAAW,IACzD;AACJ,QAAM,OAAO,WAAW,CAAC,KAAK;AAC9B,MAAI,CAAC,KAAM,QAAO;AAClB,QAAM,YAAa,KAAK,kBAAkB,KAAK;AAC/C,QAAM,SAAS,cAAc,QAAQ,cAAc,SAAY,OAAO,OAAO,SAAS;AACtF,MAAI,WAAW,QAAQ,CAAC,OAAO,SAAS,MAAM,EAAG,QAAO;AACxD,QAAM,WAAY,KAAK,gBAAkC;AACzD,QAAM,eAAgB,KAAK,eAAiC;AAC5D,MAAI,CAAC,YAAY,CAAC,aAAc,QAAO;AACvC,SAAO,EAAE,QAAQ,UAAU,aAAa,aAAa;AACvD;AAEA,eAAe,wBACb,KACA,QACA,aACwD;AACxD,QAAM,iBAAiB,sBAAsB,GAAG;AAChD,MAAI,CAAC,eAAgB,QAAO;AAC5B,QAAM,YAAY,OAAO,OAAO;AAChC,MAAI,CAAC,UAAU,OAAQ,QAAO;AAC9B,QAAM,aAAa,cACf,UAAU,OAAO,CAAC,QAAQ,IAAI,gBAAgB,WAAW,IACzD;AACJ,MAAI,CAAC,WAAW,OAAQ,QAAO;AAC/B,MAAI;AACF,UAAM,iBAAiC,EAAE,UAAU,GAAG,MAAM,oBAAI,KAAK,EAAE;AACvE,UAAM,WAAW,MAAM,eAAe;AAAA,MACpC;AAAA,MACA;AAAA,IACF;AACA,QAAI,CAAC,SAAU,QAAO;AACtB,UAAM,YAAc,SAAiB,kBAAmB,SAAiB;AAKzE,UAAM,SAAS,cAAc,QAAQ,cAAc,SAAY,OAAO,OAAO,SAAS;AACtF,QAAI,WAAW,QAAQ,CAAC,OAAO,SAAS,MAAM,EAAG,QAAO;AACxD,UAAM,WAAa,SAAiB,gBAAkC;AACtE,UAAM,eAAiB,SAAiB,gBACjC,SAAiB,aAAa,OAAQ,SAAiB,cAAc,WACrE,SAAiB,UAAU,KAC3B,SAAiB;AACxB,QAAI,CAAC,YAAY,CAAC,aAAc,QAAO;AACvC,WAAO,EAAE,QAAQ,UAAU,aAAa,aAAa;AAAA,EACvD,QAAQ;AAIN,WAAO;AAAA,EACT;AACF;AAEA,MAAM,6BAAsD;AAAA,EAC1D,MAAM;AAAA,EACN,aAAa;AAAA,EACb,aACE;AAAA,EACF,aAAa;AAAA,EACb,kBAAkB,CAAC,wBAAwB;AAAA,EAC3C,MAAM,CAAC,QAAQ,WAAW,aAAa,iBAAiB,SAAS;AAAA,EACjE,YAAY;AAAA,EACZ,SAAS,OAAO,UAAU,QAAQ;AAChC,UAAM,EAAE,SAAS,IAAI,kBAAkB,GAAG;AAC1C,UAAM,QAAQ,4BAA4B,MAAM,QAAQ;AACxD,UAAM,MAAM,MAAM,mBAAmB,KAAK,UAAU,MAAM,SAAS;AACnE,QAAI,CAAC,IAAI,OAAO;AACd,aAAO,EAAE,OAAO,OAAgB,WAAW,MAAM,UAAU;AAAA,IAC7D;AACA,UAAM,gBAAgB,MAAM,wBAAwB,KAAK,IAAI,QAAQ,MAAM,WAAW;AACtF,UAAM,eACJ,kBAAkB,yBACd,uBAAuB,IAAI,QAAQ,MAAM,WAAW,IACpD;AACN,UAAM,mBAAmB,MAAM,mBAAmB,KAAK,EAAE,OAAO,IAAI,GAAG,QAAQ;AAC/E,UAAM,sBAAsB,iBAAiB,MAAM,IAAI,CAAC,SAAS;AAAA,MAC/D,IAAI,IAAI;AAAA,MACR,MAAM,IAAI;AAAA,MACV,OAAO,IAAI,iBAAkB,iBAA4B;AAAA,IAC3D,EAAE;AACF,WAAO;AAAA,MACL,OAAO;AAAA,MACP,UAAU;AAAA,QACR;AAAA,QACA,eAAe;AAAA,UACb,QAAQ,cAAc,UAAU;AAAA,UAChC,UAAU,cAAc,aAAa,IAAI,OAAO,QAAQ,uBAAuB;AAAA,UAC/E,aAAa,cAAc,gBAAgB,MAAM,eAAe;AAAA,QAClE;AAAA,QACA,WAAW;AAAA,QACX,aAAa;AAAA,UACX,yBAAyB;AAAA,UACzB,mBAAmB;AAAA,QACrB;AAAA,MACF;AAAA,MACA,SAAS;AAAA,QACP,SAAS,IAAI;AAAA,QACb,QAAQ,MAAM;AAAA,QACd;AAAA,MACF;AAAA,MACA,wBAAwB;AAAA,QACtB,YAAY;AAAA,QACZ,YAAY,aAAa,gCAAgC,+BAA+B;AAAA,MAC1F;AAAA,IACF;AAAA,EACF;AACF;AAMO,MAAM,mBAA8C;AAAA,EACzD;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF;AAEA,IAAO,yBAAQ;",
6
+ "names": []
7
+ }
@@ -0,0 +1,167 @@
1
+ import { z } from "zod";
2
+ import { findOneWithDecryption, findWithDecryption } from "@open-mercato/shared/lib/encryption/find";
3
+ import { loadCustomFieldValues } from "@open-mercato/shared/lib/crud/custom-fields";
4
+ import { E } from "../../../generated/entities.ids.generated.js";
5
+ import { CatalogProductCategory } from "../data/entities.js";
6
+ import { assertTenantScope } from "./types.js";
7
+ function resolveEm(ctx) {
8
+ return ctx.container.resolve("em");
9
+ }
10
+ function buildScope(ctx, tenantId) {
11
+ return { tenantId, organizationId: ctx.organizationId };
12
+ }
13
+ const listCategoriesInput = z.object({
14
+ parentId: z.union([z.string().uuid(), z.null()]).optional().describe("Parent category id; pass `null` to list root nodes. Omit to list every category in scope."),
15
+ limit: z.number().int().min(1).max(100).optional().describe("Max rows (default 50, max 100)."),
16
+ offset: z.number().int().min(0).optional().describe("Rows to skip (default 0)."),
17
+ includeArchived: z.boolean().optional().describe("When true, include soft-deleted categories. Defaults to active-only.")
18
+ }).passthrough();
19
+ const listCategoriesTool = {
20
+ name: "catalog.list_categories",
21
+ displayName: "List categories",
22
+ description: "List catalog categories scoped to tenant + organization. Use `parentId: null` to list roots or a specific uuid to fetch direct children.",
23
+ inputSchema: listCategoriesInput,
24
+ requiredFeatures: ["catalog.categories.view"],
25
+ tags: ["read", "catalog"],
26
+ handler: async (rawInput, ctx) => {
27
+ const { tenantId } = assertTenantScope(ctx);
28
+ const input = listCategoriesInput.parse(rawInput);
29
+ const em = resolveEm(ctx);
30
+ const limit = input.limit ?? 50;
31
+ const offset = input.offset ?? 0;
32
+ const where = { tenantId };
33
+ if (ctx.organizationId) where.organizationId = ctx.organizationId;
34
+ if (!input.includeArchived) where.deletedAt = null;
35
+ if ("parentId" in input) {
36
+ where.parentId = input.parentId ?? null;
37
+ }
38
+ const [rows, total] = await Promise.all([
39
+ findWithDecryption(
40
+ em,
41
+ CatalogProductCategory,
42
+ where,
43
+ { limit, offset, orderBy: { depth: "asc", name: "asc" } },
44
+ buildScope(ctx, tenantId)
45
+ ),
46
+ em.count(CatalogProductCategory, where)
47
+ ]);
48
+ const filtered = rows.filter((row) => row.tenantId === tenantId);
49
+ return {
50
+ items: filtered.map((row) => ({
51
+ id: row.id,
52
+ name: row.name,
53
+ slug: row.slug ?? null,
54
+ description: row.description ?? null,
55
+ parentId: row.parentId ?? null,
56
+ rootId: row.rootId ?? null,
57
+ treePath: row.treePath ?? null,
58
+ depth: row.depth,
59
+ childIds: Array.isArray(row.childIds) ? row.childIds : [],
60
+ isActive: !!row.isActive,
61
+ organizationId: row.organizationId ?? null,
62
+ tenantId: row.tenantId ?? null,
63
+ createdAt: row.createdAt ? new Date(row.createdAt).toISOString() : null
64
+ })),
65
+ total,
66
+ limit,
67
+ offset
68
+ };
69
+ }
70
+ };
71
+ const getCategoryInput = z.object({
72
+ categoryId: z.string().uuid().describe("Category id (UUID)."),
73
+ includeRelated: z.boolean().optional().describe(
74
+ "When true, include direct children (capped at 100) and inherited ancestor refs. Custom fields are always included."
75
+ )
76
+ });
77
+ const getCategoryTool = {
78
+ name: "catalog.get_category",
79
+ displayName: "Get category",
80
+ description: "Fetch a catalog category by id with core fields and (optionally) children + ancestor inheritance + custom fields. Returns { found: false } when missing or cross-tenant.",
81
+ inputSchema: getCategoryInput,
82
+ requiredFeatures: ["catalog.categories.view"],
83
+ tags: ["read", "catalog"],
84
+ handler: async (rawInput, ctx) => {
85
+ const { tenantId } = assertTenantScope(ctx);
86
+ const input = getCategoryInput.parse(rawInput);
87
+ const em = resolveEm(ctx);
88
+ const where = {
89
+ id: input.categoryId,
90
+ tenantId,
91
+ deletedAt: null
92
+ };
93
+ if (ctx.organizationId) where.organizationId = ctx.organizationId;
94
+ const category = await findOneWithDecryption(
95
+ em,
96
+ CatalogProductCategory,
97
+ where,
98
+ void 0,
99
+ buildScope(ctx, tenantId)
100
+ );
101
+ if (!category || category.tenantId !== tenantId) {
102
+ return { found: false, categoryId: input.categoryId };
103
+ }
104
+ const customFieldValues = await loadCustomFieldValues({
105
+ em,
106
+ entityId: E.catalog.catalog_product_category,
107
+ recordIds: [category.id],
108
+ tenantIdByRecord: { [category.id]: category.tenantId ?? null },
109
+ organizationIdByRecord: { [category.id]: category.organizationId ?? null },
110
+ tenantFallbacks: [category.tenantId ?? tenantId].filter((value) => !!value)
111
+ });
112
+ const customFields = customFieldValues[category.id] ?? {};
113
+ let related = null;
114
+ if (input.includeRelated) {
115
+ const scope = buildScope(ctx, tenantId);
116
+ const children = await findWithDecryption(
117
+ em,
118
+ CatalogProductCategory,
119
+ { tenantId, parentId: category.id, deletedAt: null },
120
+ { limit: 100, orderBy: { name: "asc" } },
121
+ scope
122
+ );
123
+ related = {
124
+ children: children.filter((row) => row.tenantId === tenantId).map((row) => ({
125
+ id: row.id,
126
+ name: row.name,
127
+ slug: row.slug ?? null,
128
+ depth: row.depth,
129
+ isActive: !!row.isActive
130
+ })),
131
+ ancestorIds: Array.isArray(category.ancestorIds) ? [...category.ancestorIds] : [],
132
+ descendantIds: Array.isArray(category.descendantIds) ? [...category.descendantIds] : []
133
+ };
134
+ }
135
+ return {
136
+ found: true,
137
+ category: {
138
+ id: category.id,
139
+ name: category.name,
140
+ slug: category.slug ?? null,
141
+ description: category.description ?? null,
142
+ parentId: category.parentId ?? null,
143
+ rootId: category.rootId ?? null,
144
+ treePath: category.treePath ?? null,
145
+ depth: category.depth,
146
+ childIds: Array.isArray(category.childIds) ? [...category.childIds] : [],
147
+ ancestorIds: Array.isArray(category.ancestorIds) ? [...category.ancestorIds] : [],
148
+ descendantIds: Array.isArray(category.descendantIds) ? [...category.descendantIds] : [],
149
+ metadata: category.metadata ?? null,
150
+ isActive: !!category.isActive,
151
+ organizationId: category.organizationId ?? null,
152
+ tenantId: category.tenantId ?? null,
153
+ createdAt: category.createdAt ? new Date(category.createdAt).toISOString() : null,
154
+ updatedAt: category.updatedAt ? new Date(category.updatedAt).toISOString() : null
155
+ },
156
+ customFields,
157
+ related
158
+ };
159
+ }
160
+ };
161
+ const categoriesAiTools = [listCategoriesTool, getCategoryTool];
162
+ var categories_pack_default = categoriesAiTools;
163
+ export {
164
+ categoriesAiTools,
165
+ categories_pack_default as default
166
+ };
167
+ //# sourceMappingURL=categories-pack.js.map