@solumflow-app/crm-mcp-server 0.4.0 → 0.5.0

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.
package/dist/index.js CHANGED
@@ -6,10 +6,10 @@ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"
6
6
  import { createClient } from "@solumflow-app/crm-client";
7
7
 
8
8
  // package.json
9
- var version = "0.4.0";
9
+ var version = "0.5.0";
10
10
 
11
- // src/catalog-tools.ts
12
- import { z as z3 } from "zod";
11
+ // src/catalog-migration-tools.ts
12
+ import { z as z4 } from "zod";
13
13
 
14
14
  // ../features/crm-invoicing/src/schema/catalog-api.schema.ts
15
15
  import * as z from "zod";
@@ -43,6 +43,12 @@ var CatalogPriceSchema = z.object({
43
43
  /** The price's own id at the source, so a renamed price stays itself. */
44
44
  externalRef: CatalogExternalRefSchema.optional(),
45
45
  label: z.string().trim().min(1).max(120),
46
+ /**
47
+ * ISO 4217. Left out, the price is in the product's own currency. A
48
+ * price in another currency is the fixed price for that currency: it
49
+ * wins over a converted one and is never the default.
50
+ */
51
+ currency: z.string().regex(/^[A-Z]{3}$/).optional(),
46
52
  amountCents: cents,
47
53
  compareAtCents: cents.nullable().optional(),
48
54
  kind: z.enum(["one_off", "recurring"]).optional(),
@@ -78,21 +84,64 @@ var pricesSchema = z.array(CatalogPriceSchema).min(1).max(50).superRefine((price
78
84
  message: "Two prices carry the same externalRef."
79
85
  });
80
86
  }
81
- const labels = prices.map((price) => price.label.toLowerCase());
87
+ const labels = prices.map(
88
+ (price) => `${price.currency ?? ""} ${price.label.toLowerCase()}`
89
+ );
82
90
  if (new Set(labels).size !== labels.length) {
83
91
  ctx.addIssue({
84
92
  code: "custom",
85
93
  // i18n-exempt: a public API's field error, returned verbatim to an integration's own code and never shown in this app
86
- message: "Two prices carry the same label."
94
+ message: "Two prices carry the same label in the same currency."
87
95
  });
88
96
  }
89
97
  });
98
+ var HttpsUrlSchema = z.string().max(2048).url().refine((url) => url.startsWith("https://"), {
99
+ // i18n-exempt: a public API's field error, returned verbatim to an integration's own code and never shown in this app
100
+ message: "A file is fetched over https only."
101
+ });
102
+ var UploadPathSchema = z.string().min(1).max(200);
103
+ var CatalogFileRefSchema = z.union([
104
+ z.object({ url: HttpsUrlSchema }).strict(),
105
+ z.object({ upload: UploadPathSchema }).strict()
106
+ ]);
107
+ var IMAGE_ROLE = /^[a-z][a-z0-9_-]{0,31}$/;
108
+ var CatalogImageDetailsSchema = z.record(
109
+ z.string().min(1).max(64),
110
+ z.union([z.string().max(500), z.number().finite(), z.boolean()])
111
+ ).refine((details) => JSON.stringify(details).length <= 1800, {
112
+ // i18n-exempt: a public API's field error, returned verbatim to an integration's own code and never shown in this app
113
+ message: "Details are at most about 2 KB."
114
+ });
90
115
  var CatalogImageSchema = z.object({
91
- url: z.string().max(2048).url().refine((url) => url.startsWith("https://"), {
116
+ url: HttpsUrlSchema.optional(),
117
+ upload: UploadPathSchema.optional(),
118
+ alt: text(300),
119
+ /**
120
+ * What the picture is of, in the account's own words: `work`, `detail`,
121
+ * `insitu`. Left out keeps the role a stored picture has; `null` clears it.
122
+ */
123
+ role: z.string().regex(IMAGE_ROLE, {
124
+ message: (
125
+ // i18n-exempt: a public API's field error, returned verbatim to an integration's own code and never shown in this app
126
+ "A role starts with a letter and holds lower-case letters, digits, hyphens and underscores, at most 32."
127
+ )
128
+ }).nullable().optional(),
129
+ details: CatalogImageDetailsSchema.optional()
130
+ }).strict().refine(
131
+ (image) => image.url === void 0 !== (image.upload === void 0),
132
+ {
92
133
  // i18n-exempt: a public API's field error, returned verbatim to an integration's own code and never shown in this app
93
- message: "A picture is fetched over https only."
94
- }),
95
- alt: text(300)
134
+ message: "A picture names either a url or an upload, not both."
135
+ }
136
+ );
137
+ var CatalogStockSchema = z.object({
138
+ managed: z.boolean(),
139
+ /** Left out, the count stays as it is. */
140
+ quantity: z.number().int().min(0).max(1e9).optional(),
141
+ /** Below this a product counts as running low; `null` warns never. */
142
+ lowThreshold: z.number().int().min(0).max(1e9).nullable().optional(),
143
+ /** Whether it can still be ordered at zero: `deny`, `allow`, or `allow_notify`. */
144
+ backorders: z.enum(["deny", "allow", "allow_notify"]).optional()
96
145
  }).strict();
97
146
  var CatalogVariantAxisSchema = z.object({
98
147
  slug: CatalogFieldSlugSchema,
@@ -108,6 +157,8 @@ var CatalogVariantSchema = z.object({
108
157
  sku: text(100),
109
158
  gtin: z.string().regex(/^[0-9]{8,14}$/).nullable().optional(),
110
159
  prices: pricesSchema.optional(),
160
+ /** The variant's own shelf; a physical product counts per variant. */
161
+ stock: CatalogStockSchema.optional(),
111
162
  position: z.number().int().min(0).max(1e4).optional(),
112
163
  active: z.boolean().optional()
113
164
  }).strict();
@@ -139,6 +190,15 @@ var CatalogProductUpsertSchema = z.object({
139
190
  visibility: z.enum(["public", "unlisted"]).optional(),
140
191
  metaTitle: text(300),
141
192
  metaDescription: text(1e3),
193
+ /**
194
+ * What a shared link to the product shows (the og tags): its own title,
195
+ * description and picture. Left out, a link falls back to the name, the
196
+ * description and the first picture. `socialImage` is fetched after the
197
+ * answer like `images`; `null` takes it away.
198
+ */
199
+ socialTitle: text(300),
200
+ socialDescription: text(1e3),
201
+ socialImage: CatalogFileRefSchema.nullable().optional(),
142
202
  /** Replaces the product's categories, by slug. */
143
203
  categories: z.array(CatalogWebSlugSchema).max(50).optional(),
144
204
  /**
@@ -157,6 +217,11 @@ var CatalogProductUpsertSchema = z.object({
157
217
  addMissingOptions: z.boolean().optional(),
158
218
  /** Replaces the product's prices as a whole. */
159
219
  prices: pricesSchema.optional(),
220
+ /**
221
+ * The product's own shelf. Physical products only, and only one without
222
+ * variants: a product with variants counts per variant.
223
+ */
224
+ stock: CatalogStockSchema.optional(),
160
225
  /** Replaces the product's pictures, in this order. */
161
226
  images: z.array(CatalogImageSchema).max(20).optional(),
162
227
  variantAxes: z.array(CatalogVariantAxisSchema).min(1).max(3).optional(),
@@ -219,6 +284,14 @@ var CatalogProductUpsertSchema = z.object({
219
284
  });
220
285
  }
221
286
  }
287
+ if (body.stock !== void 0 && body.variantAxes !== void 0) {
288
+ ctx.addIssue({
289
+ code: "custom",
290
+ path: ["stock"],
291
+ // i18n-exempt: a public API's field error, returned verbatim to an integration's own code and never shown in this app
292
+ message: "A product with variants counts its stock per variant."
293
+ });
294
+ }
222
295
  if (body.availability === "scheduled" && !body.publishedAt) {
223
296
  ctx.addIssue({
224
297
  code: "custom",
@@ -230,6 +303,15 @@ var CatalogProductUpsertSchema = z.object({
230
303
  });
231
304
  var CatalogProductArchiveSchema = z.object({ source: CatalogSourceSchema }).strict();
232
305
  var CatalogCategoryUpsertSchema = z.object({
306
+ /**
307
+ * The category's id in the system it came from -- a series in a site's
308
+ * own CMS -- with `source` naming whose id it is. With both, a category
309
+ * that carries this reference is changed, and renamed to the address in
310
+ * the path when somebody gave it another one in the app; without, the
311
+ * address alone finds it, as before.
312
+ */
313
+ source: CatalogSourceSchema.optional(),
314
+ externalRef: CatalogExternalRefSchema.optional(),
233
315
  name: z.string().trim().min(1).max(200).optional(),
234
316
  description: text(5e3),
235
317
  /** `null` puts the category at the top of the tree. */
@@ -238,7 +320,14 @@ var CatalogCategoryUpsertSchema = z.object({
238
320
  publishedAt: z.string().datetime({ offset: true }).nullable().optional(),
239
321
  metaTitle: text(300),
240
322
  metaDescription: text(1e3)
241
- }).strict();
323
+ }).strict().refine(
324
+ (body) => body.source === void 0 === (body.externalRef === void 0),
325
+ {
326
+ path: ["externalRef"],
327
+ // i18n-exempt: a public API's field error, returned verbatim to an integration's own code and never shown in this app
328
+ message: "A category reference names both source and externalRef."
329
+ }
330
+ );
242
331
  var CatalogObjectSlugSchema = z.string().max(64).regex(FIELD_SLUG, {
243
332
  message: (
244
333
  // i18n-exempt: a public API's field error, returned verbatim to an integration's own code and never shown in this app
@@ -256,6 +345,7 @@ var CatalogFieldTypeSchema = z.enum([
256
345
  "number",
257
346
  "select",
258
347
  "checkbox",
348
+ "color",
259
349
  "url",
260
350
  "date",
261
351
  "content_text",
@@ -286,7 +376,18 @@ var CatalogAttributeUpsertSchema = z.object({
286
376
  * a configurator's whole structure. An item field keeps its type once it
287
377
  * exists.
288
378
  */
289
- config: z.record(z.string(), z.unknown()).optional()
379
+ config: z.record(z.string(), z.unknown()).optional(),
380
+ /**
381
+ * Say what this body would change -- a new field, a new label, choices
382
+ * added, choices taken away and how many products hold each -- and
383
+ * write nothing.
384
+ */
385
+ dryRun: z.boolean().optional(),
386
+ /**
387
+ * Bring the field back from the trash under its own id, with every value
388
+ * the products kept. Nothing else may be sent with it.
389
+ */
390
+ restore: z.literal(true).optional()
290
391
  }).strict();
291
392
  var CONFIGURATOR_FILE_KINDS = ["model", "texture"];
292
393
  var CONFIGURATOR_TEXTURE_CONTENT_TYPES = [
@@ -312,6 +413,38 @@ var ConfiguratorFileUploadSchema = z.discriminatedUnion("step", [
312
413
  uploadPath: z.string().max(200)
313
414
  }).strict()
314
415
  ]);
416
+ var CATALOG_FILE_KINDS = ["image", "file"];
417
+ var CATALOG_FILE_CONTENT_TYPES = [
418
+ "image/png",
419
+ "image/jpeg",
420
+ "image/webp",
421
+ "application/pdf",
422
+ "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
423
+ "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
424
+ "application/vnd.openxmlformats-officedocument.presentationml.presentation",
425
+ "text/plain",
426
+ "text/csv",
427
+ "application/zip"
428
+ ];
429
+ var CatalogFileUploadSchema = z.discriminatedUnion("step", [
430
+ z.object({
431
+ step: z.literal("start"),
432
+ /** A picture (2 MB, png/jpeg/webp) or a download for a list item (20 MB). */
433
+ kind: z.enum(CATALOG_FILE_KINDS),
434
+ /** The bytes about to be sent, so a too-large file is refused before it travels. */
435
+ size: z.number().int().min(1),
436
+ contentType: z.enum(CATALOG_FILE_CONTENT_TYPES)
437
+ }).strict(),
438
+ z.object({
439
+ step: z.literal("finish"),
440
+ kind: z.enum(CATALOG_FILE_KINDS),
441
+ /** The `uploadPath` `start` answered with. */
442
+ uploadPath: z.string().max(200)
443
+ }).strict()
444
+ ]);
445
+
446
+ // src/catalog-tools.ts
447
+ import { z as z3 } from "zod";
315
448
 
316
449
  // src/tool-answers.ts
317
450
  import { CrmApiError } from "@solumflow-app/crm-client";
@@ -342,7 +475,7 @@ function refusal(error, scope) {
342
475
  var NEXT_STEP = {
343
476
  unauthorized: "The key is not recognised or was revoked. Ask the account owner for a new secret key.",
344
477
  feature_unavailable: "The module this needs is switched off for the account. Ask somebody to switch it on in the app.",
345
- rate_limited: "Too many requests. Wait a moment and try again.",
478
+ rate_limited: "Too many requests: 120 a minute per key, 600 per account, and 5 MB of bodies a day per key. Wait the seconds the answer names and try again.",
346
479
  internal_error: "Something went wrong in the app. Trying again is safe."
347
480
  };
348
481
  function codeBlock(snippet) {
@@ -357,8 +490,8 @@ var page = {
357
490
  function more(nextCursor) {
358
491
  return nextCursor ? `More on the next page: cursor ${nextCursor}` : "";
359
492
  }
360
- function money(cents2, currency) {
361
- return `${(cents2 / 100).toFixed(2)} ${currency.toUpperCase()}`;
493
+ function money(cents2, currency2) {
494
+ return `${(cents2 / 100).toFixed(2)} ${currency2.toUpperCase()}`;
362
495
  }
363
496
 
364
497
  // src/catalog-tools.ts
@@ -368,6 +501,9 @@ var objectInput = {
368
501
  )
369
502
  };
370
503
  var CONTENT_BLOCKS = 'Besides plain values (text, number, choice), a product can carry content blocks, three field types: "content_text" is { html, links? } with already-sanitised html; "cta" is { heading, body, primary: {label,url}, secondary? }, a call to action; "repeater" is { items: [{ id, ...one value per item field }] } and covers lists, accordions (FAQ), downloads and testimonials. A repeater\u2019s field says how to draw it: "display" is "list" or "accordion", "preset" names where it started (list, faq, downloads, testimonials), "itemFields" is the structure of one item (key, label, type: text, richtext, url {label,url}, icon {set,name}, image or file {url,name,size,contentType}) and "titleKey" is the item field that names an item, such as the question of a FAQ. A "configurator" is a 3D product configurator: a listing carries only { configurator: true }, a product\u2019s own page the whole document (see read_configurator). A block that is public but empty is null; a block that is not public is not there at all.';
504
+ var currency = z3.string().regex(/^[A-Z]{3}$/).optional().describe(
505
+ "Every price in this currency (see list_currencies), each with its origin: base, fixed or converted. Left out, every price in its own currency."
506
+ );
371
507
  var slugOrId = z3.string().min(1).max(200).describe("The address (slug) or the id of the product.");
372
508
  function registerCatalogTools(server, client) {
373
509
  server.registerTool(
@@ -412,17 +548,46 @@ function registerCatalogTools(server, client) {
412
548
  }
413
549
  }
414
550
  );
551
+ server.registerTool(
552
+ "list_currencies",
553
+ {
554
+ title: "List the currencies the shop sells in",
555
+ description: "The account\u2019s own currency first, then the ones it switched on, with a name and symbol: what a currency menu on a website is built from. Pass one as `currency` to get_product or list_products for prices in it. Which currencies are offered, and at what rate, is set in the app (Settings \u2192 Verkoop \u2192 Valuta), not through this tool.",
556
+ inputSchema: {
557
+ locale: z3.string().min(2).max(35).optional().describe('The language names are written in, such as "nl".')
558
+ },
559
+ annotations: { readOnlyHint: true, idempotentHint: true }
560
+ },
561
+ async ({ locale }) => {
562
+ try {
563
+ const currencies = await client.getCurrencies(
564
+ locale ? { locale } : void 0
565
+ );
566
+ return answer(
567
+ currencies.map(
568
+ (option) => `${option.code} (${option.symbol}) ${option.name}${option.isDefault ? ", the account\u2019s own" : ""}`
569
+ ).join("\n"),
570
+ currencies
571
+ );
572
+ } catch (error) {
573
+ return refusal(error, "catalog:read");
574
+ }
575
+ }
576
+ );
415
577
  server.registerTool(
416
578
  "get_product",
417
579
  {
418
580
  title: "Read one product",
419
- description: "One product as its page needs it: name, prices, pictures, categories, variants and `fields`, keyed by field slug. May be a minute old. Answers that it is not found for an unknown product. " + CONTENT_BLOCKS,
420
- inputSchema: { slugOrId },
581
+ description: "One product as its page needs it: name, prices, pictures, categories, variants and `fields`, keyed by field slug. Published products only, and may be a minute old: to read back what you just wrote, use get_product_by_ref. Answers that it is not found for an unknown product. " + CONTENT_BLOCKS,
582
+ inputSchema: { slugOrId, currency },
421
583
  annotations: { readOnlyHint: true, idempotentHint: true }
422
584
  },
423
- async ({ slugOrId: wanted }) => {
585
+ async ({ slugOrId: wanted, currency: inCurrency }) => {
424
586
  try {
425
- const product = await client.getProduct(wanted);
587
+ const product = await client.getProduct(
588
+ wanted,
589
+ inCurrency ? { currency: inCurrency } : void 0
590
+ );
426
591
  if (!product) {
427
592
  return answer(`No product "${wanted}" on this account.`, null);
428
593
  }
@@ -440,19 +605,37 @@ function registerCatalogTools(server, client) {
440
605
  "list_products",
441
606
  {
442
607
  title: "List the published products",
443
- description: "One page of the catalogue: name, address (slug), lowest price, whether it is in stock, its first picture and the public fields a listing shows. Narrow it to one category\u2019s whole branch by its id (from list_categories), or to one catalogue by the slug of its object. Read get_product for everything a product page needs. May be a minute old.",
608
+ description: "One page of the catalogue: name, address (slug), lowest price, whether it is in stock, its first picture and the public fields a listing shows. Narrow it to one category\u2019s whole branch by its id or address, or by the reference upsert_category recorded (`categorySource` + `categoryRef`), to one catalogue by the slug of its object, or to the product carrying an article number (`sku`). Read get_product for everything a product page needs. May be a minute old.",
444
609
  inputSchema: {
445
610
  ...page,
446
- category: z3.string().uuid().optional().describe("A category id; includes its subcategories."),
611
+ category: z3.string().regex(
612
+ /^([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}|[a-z0-9]+(-[a-z0-9]+)*)$/i
613
+ ).max(64).optional().describe(
614
+ "A category id or address (slug); includes its subcategories."
615
+ ),
616
+ categorySource: z3.string().max(64).optional().describe("With categoryRef: whose id the category reference is."),
617
+ categoryRef: z3.string().max(255).optional().describe("A category by the id it has in your own system."),
618
+ sku: z3.string().min(1).max(100).optional().describe(
619
+ "The product carrying this article number, or whose variant does."
620
+ ),
447
621
  object: z3.string().min(1).max(63).optional().describe(
448
622
  'One catalogue, by the slug of its object, such as "product".'
449
- )
623
+ ),
624
+ currency
450
625
  },
451
626
  annotations: { readOnlyHint: true, idempotentHint: true }
452
627
  },
453
- async (input) => {
628
+ async ({ categorySource, categoryRef, ...input }) => {
454
629
  try {
455
- const list = await client.getProducts(input);
630
+ const list = await client.getProducts({
631
+ ...input,
632
+ ...categorySource && categoryRef ? {
633
+ categoryRef: {
634
+ source: categorySource,
635
+ externalRef: categoryRef
636
+ }
637
+ } : {}
638
+ });
456
639
  return answer(
457
640
  list.data.length === 0 ? "No published products match." : [...list.data.map(describeProduct), more(list.nextCursor)].filter(Boolean).join("\n"),
458
641
  list
@@ -542,7 +725,8 @@ function registerCatalogTools(server, client) {
542
725
  }
543
726
  function describeProduct(product) {
544
727
  const stock = product.inStock ? "" : ", out of stock";
545
- return `- ${product.name} (${product.slug}): from ${money(product.priceFromCents, product.currency)}${stock}`;
728
+ const from = product.priceFromCents === null ? `no price in ${product.currency}` : `from ${money(product.priceFromCents, product.currency)}`;
729
+ return `- ${product.name} (${product.slug}): ${from}${stock}`;
546
730
  }
547
731
  function describeCategory(category) {
548
732
  const path = category.path.map((step) => step.name).join(" / ");
@@ -558,18 +742,247 @@ function describeField(field) {
558
742
  return `- ${field.slug}: ${field.label}, repeater (${field.display ?? "list"}${preset}; items: ${items}${title})`;
559
743
  }
560
744
 
561
- // src/catalog-write-tools.ts
745
+ // src/catalog-migration-tools.ts
746
+ import { readFile, stat } from "fs/promises";
747
+ import { extname } from "path";
748
+ var BULK_LIMIT = 25;
749
+ var LIMITS = "Limits: 120 requests a minute per key and 600 per account; 5 MB of request bodies a day per key and 20 MB per account (a long HTML description counts). A refusal for speed says how long to wait.";
750
+ var FILE_RULES = {
751
+ image: {
752
+ maxBytes: 2 * 1024 * 1024,
753
+ types: {
754
+ ".png": "image/png",
755
+ ".jpg": "image/jpeg",
756
+ ".jpeg": "image/jpeg",
757
+ ".webp": "image/webp"
758
+ }
759
+ },
760
+ file: {
761
+ maxBytes: 20 * 1024 * 1024,
762
+ types: {
763
+ ".pdf": "application/pdf",
764
+ ".docx": "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
765
+ ".xlsx": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
766
+ ".pptx": "application/vnd.openxmlformats-officedocument.presentationml.presentation",
767
+ ".txt": "text/plain",
768
+ ".csv": "text/csv",
769
+ ".zip": "application/zip"
770
+ }
771
+ }
772
+ };
562
773
  function target(object2) {
563
774
  return object2 ? { object: object2 } : void 0;
564
775
  }
565
- var FIELD_KINDS = 'Kinds: text, number, select, checkbox, url, date, and three content blocks for a product page: "content_text" (formatted text, no config), "cta" (a call to action; config { secondary: true } offers a second button) and "repeater" (a list, an accordion such as a FAQ, downloads or testimonials; config { display: "list" | "accordion", preset?: "list" | "faq" | "downloads" | "testimonials", itemFields: [{ key, label, type: text | richtext | url | icon | image | file }], titleKey?: the item field that names an item }). A number may take config { prefix?, suffix? }. A configurator is made with upsert_configurator. The kind is fixed once the field exists, and so is the type of a repeater\u2019s item field.';
776
+ function notDone(text2) {
777
+ return { isError: true, content: [{ type: "text", text: text2 }] };
778
+ }
779
+ function registerCatalogMigrationTools(server, client) {
780
+ server.registerTool(
781
+ "get_product_by_ref",
782
+ {
783
+ title: "Read back a product you wrote",
784
+ description: "One product by the reference it was written under (`source` + `externalRef`), a draft too: its page, its sku and shelf per variant, the shared-link texts and picture, and `files` -- for every picture or file of the last write whether it is `pending`, `stored` or `refused`, with a reason code. Live, never cached: read this after upsert_product to see what arrived.",
785
+ inputSchema: {
786
+ externalRef: CatalogExternalRefSchema,
787
+ source: CatalogSourceSchema,
788
+ ...objectInput
789
+ },
790
+ annotations: { readOnlyHint: true, idempotentHint: true }
791
+ },
792
+ async ({ externalRef, source, object: object2 }) => {
793
+ try {
794
+ const product = await client.getProductByRef(
795
+ externalRef,
796
+ source,
797
+ target(object2)
798
+ );
799
+ if (!product) {
800
+ return answer(
801
+ `No product with reference "${externalRef}" from "${source}".`,
802
+ null
803
+ );
804
+ }
805
+ const refused = product.files.filter(
806
+ (file) => file.status === "refused"
807
+ );
808
+ const pending = product.files.filter(
809
+ (file) => file.status === "pending"
810
+ );
811
+ return answer(
812
+ `${product.name} (${product.slug || "no address"}), ${product.availability}; ${product.images.length} pictures; files: ${pending.length} pending, ${refused.length} refused${refused.length ? ` (${refused.map((file) => `${file.ref}: ${file.reason}`).join("; ")})` : ""}.`,
813
+ product
814
+ );
815
+ } catch (error) {
816
+ return refusal(error, "catalog:write");
817
+ }
818
+ }
819
+ );
820
+ server.registerTool(
821
+ "upsert_products",
822
+ {
823
+ title: "Make or change many products",
824
+ description: `Up to ${BULK_LIMIT} products, one after another, each exactly as upsert_product takes it (\`externalRef\` beside its body). A refusal is that product\u2019s outcome and the others go on; a refusal for speed is waited out and the product sent again; a key that is refused stops the run and the rest is \`skipped\`. Answers per product \`created\`, \`changed\`, \`refused\` (with the fields) or \`skipped\`. ` + LIMITS,
825
+ inputSchema: {
826
+ products: z4.array(
827
+ z4.object({
828
+ externalRef: CatalogExternalRefSchema,
829
+ product: CatalogProductUpsertSchema
830
+ })
831
+ ).min(1).max(BULK_LIMIT),
832
+ ...objectInput
833
+ },
834
+ annotations: { destructiveHint: true, idempotentHint: true }
835
+ },
836
+ async ({ products, object: object2 }) => {
837
+ try {
838
+ const outcomes = await client.upsertProducts(
839
+ products.map((item) => ({
840
+ externalRef: item.externalRef,
841
+ input: item.product
842
+ })),
843
+ target(object2)
844
+ );
845
+ const count = (kind) => outcomes.filter((outcome) => outcome.outcome === kind).length;
846
+ const refusals = outcomes.filter((outcome) => outcome.outcome === "refused").map(
847
+ (outcome) => `- ${outcome.externalRef}: ${outcome.error?.message ?? "refused"}` + (outcome.error?.fields.length ? ` (${outcome.error.fields.map((field) => `${field.path}: ${field.message}`).join("; ")})` : "")
848
+ );
849
+ return answer(
850
+ [
851
+ `${count("created")} made, ${count("changed")} changed, ${count("refused")} refused, ${count("skipped")} skipped.`,
852
+ ...refusals
853
+ ].join("\n"),
854
+ outcomes
855
+ );
856
+ } catch (error) {
857
+ return refusal(error, "catalog:write");
858
+ }
859
+ }
860
+ );
861
+ server.registerTool(
862
+ "delete_field",
863
+ {
864
+ title: "Take a catalogue field away",
865
+ description: "Moves a field to the trash, as the app does: for thirty days it can come back with restore_field, and every product keeps its value meanwhile. Use `permanent` only when the user explicitly asks to delete for good, and first without `confirm`: that only counts the products that would lose their value. Show the user that count; send `confirm` (the slug again) only after they agree. A field of the app\u2019s own, or one a product varies along, is refused.",
866
+ inputSchema: {
867
+ slug: CatalogFieldSlugSchema,
868
+ permanent: z4.boolean().optional(),
869
+ confirm: z4.string().max(63).optional(),
870
+ ...objectInput
871
+ },
872
+ annotations: { destructiveHint: true, idempotentHint: true }
873
+ },
874
+ async ({ slug, permanent, confirm, object: object2 }) => {
875
+ try {
876
+ const result = await client.removeField(slug, {
877
+ ...target(object2),
878
+ ...permanent ? { permanent } : {},
879
+ ...confirm ? { confirm } : {}
880
+ });
881
+ const text2 = result.state === "trashed" ? `${slug} is in the trash until ${result.restorableUntil ?? "thirty days from now"}; ${result.productsWithValue} products keep their value.` : result.state === "would_delete" ? `Nothing changed. Deleting ${slug} for good would clear its value on ${result.productsWithValue} products. ${result.confirm}` : `${slug} is deleted; its value on ${result.productsWithValue} products is being cleared.`;
882
+ return answer(text2, result);
883
+ } catch (error) {
884
+ return refusal(error, "catalog:write");
885
+ }
886
+ }
887
+ );
888
+ server.registerTool(
889
+ "restore_field",
890
+ {
891
+ title: "Bring a catalogue field back from the trash",
892
+ description: "A field moved to the trash in the last thirty days comes back under its own id, with every value the products kept.",
893
+ inputSchema: { slug: CatalogFieldSlugSchema, ...objectInput },
894
+ annotations: { destructiveHint: false, idempotentHint: true }
895
+ },
896
+ async ({ slug, object: object2 }) => {
897
+ try {
898
+ const field = await client.restoreField(slug, target(object2));
899
+ return answer(`${field.label} (${field.slug}) is back.`, field);
900
+ } catch (error) {
901
+ return refusal(error, "catalog:write");
902
+ }
903
+ }
904
+ );
905
+ server.registerTool(
906
+ "get_category_by_ref",
907
+ {
908
+ title: "Find a category by its reference",
909
+ description: "The category upsert_category recorded with this `source` and `externalRef` -- the id of a series in your own system -- also after its address was changed in the app. Only a published category.",
910
+ inputSchema: {
911
+ source: CatalogSourceSchema,
912
+ externalRef: CatalogExternalRefSchema
913
+ },
914
+ annotations: { readOnlyHint: true, idempotentHint: true }
915
+ },
916
+ async ({ source, externalRef }) => {
917
+ try {
918
+ const category = await client.getCategoryByRef(source, externalRef);
919
+ return answer(
920
+ category ? `${category.name} (${category.slug}), id ${category.id}.` : `No published category with reference "${externalRef}" from "${source}".`,
921
+ category
922
+ );
923
+ } catch (error) {
924
+ return refusal(error, "catalog:read");
925
+ }
926
+ }
927
+ );
928
+ server.registerTool(
929
+ "upload_catalog_file",
930
+ {
931
+ title: "Upload a picture or a file from disk",
932
+ description: 'Reads one file from the user\u2019s disk and uploads it, for a product write to name as { "upload": "<uploadPath>" } instead of a url: a picture (png, jpeg, webp, at most 2 MB) for `images`, `socialImage` or a list item\u2019s image, or a "file" (pdf, Office, txt, csv, zip, at most 20 MB) for a list item\u2019s download. An upload is kept a day.',
933
+ inputSchema: {
934
+ kind: z4.enum(["image", "file"]),
935
+ filePath: z4.string().min(1).max(1e3).describe("The absolute path of the file on this machine.")
936
+ },
937
+ annotations: { destructiveHint: false, idempotentHint: false }
938
+ },
939
+ async ({ kind, filePath }) => {
940
+ const rules = FILE_RULES[kind];
941
+ const extension = extname(filePath).toLowerCase();
942
+ const contentType = rules.types[extension];
943
+ if (!contentType) {
944
+ return notDone(
945
+ `A ${kind} is a ${Object.keys(rules.types).join(", ")} file; this is "${extension || "no extension"}".`
946
+ );
947
+ }
948
+ const info = await stat(filePath).catch(() => null);
949
+ if (!info?.isFile()) {
950
+ return notDone(`"${filePath}" is not a file on this machine.`);
951
+ }
952
+ if (info.size > rules.maxBytes) {
953
+ return notDone(
954
+ `The file is ${info.size} bytes; a ${kind} may be at most ${rules.maxBytes}.`
955
+ );
956
+ }
957
+ try {
958
+ const result = await client.uploadCatalogFile({
959
+ kind,
960
+ bytes: new Uint8Array(await readFile(filePath)),
961
+ contentType
962
+ });
963
+ return answer(
964
+ `Uploaded. Name it as { "upload": "${result.uploadPath}" }.`,
965
+ result
966
+ );
967
+ } catch (error) {
968
+ return refusal(error, "catalog:write");
969
+ }
970
+ }
971
+ );
972
+ }
973
+
974
+ // src/catalog-write-tools.ts
975
+ function target2(object2) {
976
+ return object2 ? { object: object2 } : void 0;
977
+ }
978
+ var FIELD_KINDS = 'Kinds: text, number, select, checkbox, color (a value "#rrggbb"; upper case is stored lower case), url, date, and three content blocks for a product page: "content_text" (formatted text, no config), "cta" (a call to action; config { secondary: true } offers a second button) and "repeater" (a list, an accordion such as a FAQ, downloads or testimonials; config { display: "list" | "accordion", preset?: "list" | "faq" | "downloads" | "testimonials", itemFields: [{ key, label, type: text | richtext | url | icon | image | file | number | select }], titleKey?: the item field that names an item }). A number item may name { min?, max?, suffix? }, a select item names { options: [{ value, label }] } (at most 100); its value is one of those values. An image or file item is filled with { url } or { upload } in upsert_product, fetched after the answer. A number may take config { prefix?, suffix? }. A configurator is made with upsert_configurator. The kind is fixed once the field exists, and so is the type of a repeater\u2019s item field.';
566
979
  var IDENTITY = 'A product is known by `externalRef`, its id in the system it comes from, together with `source`, the name of that system (for example "acme-webshop"). The same pair always means the same product: sending it again changes it instead of making a second one, so use references that stay the same, never a name that may change.';
567
980
  function registerCatalogWriteTools(server, client) {
568
981
  server.registerTool(
569
982
  "upsert_product",
570
983
  {
571
984
  title: "Make or change a product",
572
- description: IDENTITY + ' Every field but `source` may be left out, and left out means "leave it as it is"; null empties a field. A new product needs a `name` and is a draft unless `availability` says otherwise: do not publish without the user asking. `prices`, `categories`, `images` and `variants` each replace what the product had as a whole, and a variant left out is archived. Categories are named by slug and a missing one is refused unless `createMissingCategories` is true; a choice a select field does not offer is refused unless `addMissingOptions` is true. Pictures are https URLs, fetched after the answer (`images: "pending"`). Amounts are in cents. Read list_catalog_fields first for the slugs `fields` takes.',
985
+ description: IDENTITY + ' Every field but `source` may be left out, and left out means "leave it as it is"; null empties a field. A new product needs a `name` and is a draft unless `availability` says otherwise: do not publish without the user asking. `prices`, `categories`, `images` and `variants` each replace what the product had as a whole, and a variant left out is archived. Categories are named by slug and a missing one is refused unless `createMissingCategories` is true; a choice a select field does not offer is refused unless `addMissingOptions` is true. Pictures are https URLs, fetched after the answer (`images: "pending"`). Amounts are in cents. A price without `currency` is in the product\'s own; one with another `currency` is the fixed price for that currency, may share its label with the euro price, and can never be the default. `stock` (physical only; per variant when there are variants) is a count, not a movement: a second run writes it again over a correction made in the app, so send it only when the source keeps the count. A picture is { url } or { upload } (upload_catalog_file), with `alt`, a `role` in your own words ("insitu") and flat `details` ({ widthCm: 120 }); `socialImage` is the shared-link picture beside `socialTitle` and `socialDescription`. Files arrive after the answer: get_product_by_ref says per file whether it came. `description` is plain text (5,000 characters); `longDescription` is HTML, cleaned to headings, paragraphs, lists, links, bold and italic. To write many products use upsert_products. Read list_catalog_fields first for the slugs `fields` takes.',
573
986
  inputSchema: {
574
987
  externalRef: CatalogExternalRefSchema.describe(
575
988
  "The id of the product in the system named by `source`."
@@ -584,7 +997,7 @@ function registerCatalogWriteTools(server, client) {
584
997
  const product = await client.upsertProduct(
585
998
  externalRef,
586
999
  input,
587
- target(object2)
1000
+ target2(object2)
588
1001
  );
589
1002
  const verb = product.created ? "Made" : "Changed";
590
1003
  return answer(
@@ -615,7 +1028,7 @@ function registerCatalogWriteTools(server, client) {
615
1028
  const result = await client.archiveProduct(
616
1029
  externalRef,
617
1030
  source,
618
- target(object2)
1031
+ target2(object2)
619
1032
  );
620
1033
  return answer(
621
1034
  result.alreadyArchived ? "That product was already archived." : "Archived.",
@@ -630,7 +1043,7 @@ function registerCatalogWriteTools(server, client) {
630
1043
  "upsert_category",
631
1044
  {
632
1045
  title: "Make or change a category",
633
- description: "A category, known by its address (slug): sending the same slug again changes it. Left-out fields stay as they are; a new category needs a `name`. `parentSlug` places it under another category, null at the top. A new category is a draft unless `availability` says otherwise. Categories belong to one catalogue: a product names only categories of its own catalogue.",
1046
+ description: "A category, known by its address (slug): sending the same slug again changes it. Left-out fields stay as they are; a new category needs a `name`. `parentSlug` places it under another category, null at the top. A new category is a draft unless `availability` says otherwise. Categories belong to one catalogue: a product names only categories of its own catalogue. Send `source` and `externalRef` -- the id of the series in your own system -- and the category keeps that identity: it is found by it even after its address was changed in the app (and gets the address of this call back), and list_products narrows by it. Content of a series (long texts, galleries) stays in your own CMS.",
634
1047
  inputSchema: {
635
1048
  slug: CatalogWebSlugSchema.describe(
636
1049
  "The address of the category: lower case, digits and dashes."
@@ -645,7 +1058,7 @@ function registerCatalogWriteTools(server, client) {
645
1058
  const category = await client.upsertCategory(
646
1059
  slug,
647
1060
  input,
648
- target(object2)
1061
+ target2(object2)
649
1062
  );
650
1063
  return answer(
651
1064
  `${category.created ? "Made" : "Changed"} ${category.name} (${category.slug}).`,
@@ -660,7 +1073,7 @@ function registerCatalogWriteTools(server, client) {
660
1073
  "upsert_field",
661
1074
  {
662
1075
  title: "Make or change a catalogue field",
663
- description: "A custom field on products, known by its slug: sending the same slug again changes it. A new field needs a `label`. " + FIELD_KINDS + " `config` replaces the settings as a whole. `options` makes sure choices exist and never removes one; `removeOptions` takes choices off the list, which empties them on every product that had them. `public` decides whether the website may show the field at all.",
1076
+ description: "A custom field on products, known by its slug: sending the same slug again changes it. A new field needs a `label`. " + FIELD_KINDS + " `config` replaces the settings as a whole. `options` makes sure choices exist and never removes one; `removeOptions` takes choices off the list, which empties them on every product that had them. `public` decides whether the website may show the field at all. `dryRun: true` answers what would change -- and how many products hold each choice that would go -- and writes nothing. A slug in the trash is taken until restore_field or a permanent delete_field.",
664
1077
  inputSchema: {
665
1078
  slug: CatalogFieldSlugSchema.describe(
666
1079
  "The slug of the field: a lower-case letter, then lower case, digits and underscores."
@@ -672,7 +1085,12 @@ function registerCatalogWriteTools(server, client) {
672
1085
  },
673
1086
  async ({ slug, object: object2, ...input }) => {
674
1087
  try {
675
- const field = await client.upsertField(slug, input, target(object2));
1088
+ if (input.dryRun) {
1089
+ const { dryRun: _dryRun, ...rest } = input;
1090
+ const preview = await client.previewField(slug, rest, target2(object2));
1091
+ return answer(`Nothing written. ${JSON.stringify(preview)}`, preview);
1092
+ }
1093
+ const field = await client.upsertField(slug, input, target2(object2));
676
1094
  return answer(
677
1095
  `${field.created ? "Made" : "Changed"} ${field.label} (${field.slug}), ${field.type}, ${field.public ? "public" : "not public"}.`,
678
1096
  field
@@ -710,7 +1128,7 @@ function registerCatalogWriteTools(server, client) {
710
1128
  }
711
1129
 
712
1130
  // src/configurator-tools.ts
713
- import { z as z7 } from "zod";
1131
+ import { z as z8 } from "zod";
714
1132
 
715
1133
  // ../features/product-configurator/src/shared/configurator-limits.ts
716
1134
  var CONFIGURATOR_LIMITS = {
@@ -1076,16 +1494,16 @@ function evaluateFormulaSource(source, scope) {
1076
1494
  }
1077
1495
 
1078
1496
  // ../features/product-configurator/src/shared/geometry-document.schema.ts
1079
- import { z as z4 } from "zod";
1497
+ import { z as z5 } from "zod";
1080
1498
  var CONFIGURATOR_NAME_PATTERN = /^[a-z][a-z0-9_]{0,39}$/;
1081
1499
  var CONFIGURATOR_CODE_PATTERN = /^[a-z0-9][a-z0-9_-]{0,39}$/;
1082
- var ConfiguratorNameSchema = z4.string().regex(CONFIGURATOR_NAME_PATTERN, "configurator.invalidName");
1083
- var ConfiguratorCodeSchema = z4.string().regex(CONFIGURATOR_CODE_PATTERN, "configurator.invalidCode");
1084
- var FormulaSchema = z4.union([
1085
- z4.number().finite(),
1086
- z4.string().trim().min(1).max(CONFIGURATOR_LIMITS.formulaChars, "configurator.formulaTooLong")
1500
+ var ConfiguratorNameSchema = z5.string().regex(CONFIGURATOR_NAME_PATTERN, "configurator.invalidName");
1501
+ var ConfiguratorCodeSchema = z5.string().regex(CONFIGURATOR_CODE_PATTERN, "configurator.invalidCode");
1502
+ var FormulaSchema = z5.union([
1503
+ z5.number().finite(),
1504
+ z5.string().trim().min(1).max(CONFIGURATOR_LIMITS.formulaChars, "configurator.formulaTooLong")
1087
1505
  ]);
1088
- var Vector3Schema = z4.tuple([FormulaSchema, FormulaSchema, FormulaSchema]);
1506
+ var Vector3Schema = z5.tuple([FormulaSchema, FormulaSchema, FormulaSchema]);
1089
1507
  var PRIMITIVE_TYPES = [
1090
1508
  "box",
1091
1509
  "rounded_top_box",
@@ -1128,93 +1546,93 @@ var PrimitiveNodeFields = {
1128
1546
  size: Vector3Schema,
1129
1547
  at: Vector3Schema,
1130
1548
  visible: FormulaSchema.optional(),
1131
- params: z4.record(ConfiguratorNameSchema, FormulaSchema).optional()
1549
+ params: z5.record(ConfiguratorNameSchema, FormulaSchema).optional()
1132
1550
  };
1133
- var PrimitiveNodeSchema = z4.strictObject({
1551
+ var PrimitiveNodeSchema = z5.strictObject({
1134
1552
  ...PrimitiveNodeFields,
1135
- type: z4.enum(PRIMITIVE_TYPES).exclude(["model"])
1553
+ type: z5.enum(PRIMITIVE_TYPES).exclude(["model"])
1136
1554
  });
1137
- var ModelPathSchema = z4.string().regex(CONFIGURATOR_MODEL_PATH_PATTERN, "configurator.invalidModelPath");
1138
- var ServedFileUrlSchema = z4.url({ protocol: /^https?$/ });
1555
+ var ModelPathSchema = z5.string().regex(CONFIGURATOR_MODEL_PATH_PATTERN, "configurator.invalidModelPath");
1556
+ var ServedFileUrlSchema = z5.url({ protocol: /^https?$/ });
1139
1557
  function geometryNodeSchema(modelFile) {
1140
- const ModelNodeSchema = z4.strictObject({
1558
+ const ModelNodeSchema = z5.strictObject({
1141
1559
  ...PrimitiveNodeFields,
1142
- type: z4.literal("model"),
1560
+ type: z5.literal("model"),
1143
1561
  model: modelFile
1144
1562
  });
1145
- const ModelPerChoiceNodeSchema = z4.strictObject({
1563
+ const ModelPerChoiceNodeSchema = z5.strictObject({
1146
1564
  ...PrimitiveNodeFields,
1147
- type: z4.literal("model"),
1565
+ type: z5.literal("model"),
1148
1566
  follows: ConfiguratorNameSchema,
1149
- models: z4.record(ConfiguratorCodeSchema, modelFile).refine(
1567
+ models: z5.record(ConfiguratorCodeSchema, modelFile).refine(
1150
1568
  (models) => Object.keys(models).length <= CONFIGURATOR_LIMITS.presets,
1151
1569
  { message: "configurator.tooMany" }
1152
1570
  )
1153
1571
  });
1154
- const node = z4.lazy(
1155
- () => z4.union([
1572
+ const node = z5.lazy(
1573
+ () => z5.union([
1156
1574
  PrimitiveNodeSchema,
1157
1575
  ModelNodeSchema,
1158
1576
  ModelPerChoiceNodeSchema,
1159
- z4.strictObject({
1577
+ z5.strictObject({
1160
1578
  id: ConfiguratorNameSchema,
1161
- type: z4.literal("repeat"),
1579
+ type: z5.literal("repeat"),
1162
1580
  count: FormulaSchema,
1163
- index: z4.enum(["i", "j"]).optional(),
1581
+ index: z5.enum(["i", "j"]).optional(),
1164
1582
  visible: FormulaSchema.optional(),
1165
- children: z4.array(node).min(1)
1583
+ children: z5.array(node).min(1)
1166
1584
  })
1167
1585
  ])
1168
1586
  );
1169
1587
  return node;
1170
1588
  }
1171
1589
  var GeometryNodeSchema = geometryNodeSchema(ModelPathSchema);
1172
- var GeometryBindingSchema = z4.strictObject({
1590
+ var GeometryBindingSchema = z5.strictObject({
1173
1591
  param: ConfiguratorNameSchema,
1174
- map: z4.record(ConfiguratorCodeSchema, z4.number().finite()).optional()
1592
+ map: z5.record(ConfiguratorCodeSchema, z5.number().finite()).optional()
1175
1593
  });
1176
- var GeometryInputSchema = z4.strictObject({
1594
+ var GeometryInputSchema = z5.strictObject({
1177
1595
  name: ConfiguratorNameSchema,
1178
1596
  binding: GeometryBindingSchema
1179
1597
  });
1180
- var GeometrySlotSchema = z4.strictObject({
1598
+ var GeometrySlotSchema = z5.strictObject({
1181
1599
  name: ConfiguratorNameSchema,
1182
1600
  param: ConfiguratorNameSchema.optional(),
1183
1601
  material: ConfiguratorCodeSchema.optional(),
1184
- color: z4.string().regex(/^#[0-9a-f]{6}$/i, "configurator.invalidColor").optional(),
1602
+ color: z5.string().regex(/^#[0-9a-f]{6}$/i, "configurator.invalidColor").optional(),
1185
1603
  /**
1186
1604
  * Which way the grain of a picture runs on this part: `horizontal` (the
1187
1605
  * default) as the picture is drawn, `vertical` turned a quarter. It belongs
1188
1606
  * to the part, not the material: the same oak lies on a cabinet and stands
1189
1607
  * on a tall wall panel.
1190
1608
  */
1191
- grain: z4.enum(["horizontal", "vertical"]).optional()
1609
+ grain: z5.enum(["horizontal", "vertical"]).optional()
1192
1610
  });
1193
- var GeometryAnnotationSchema = z4.strictObject({
1611
+ var GeometryAnnotationSchema = z5.strictObject({
1194
1612
  id: ConfiguratorNameSchema,
1195
- axis: z4.enum(["x", "y"]),
1613
+ axis: z5.enum(["x", "y"]),
1196
1614
  from: FormulaSchema,
1197
1615
  to: FormulaSchema,
1198
1616
  at: FormulaSchema,
1199
- label: z4.string().trim().min(1).max(80).optional()
1617
+ label: z5.string().trim().min(1).max(80).optional()
1200
1618
  });
1201
1619
  function geometryDocumentSchema(modelFile) {
1202
- return z4.strictObject({
1620
+ return z5.strictObject({
1203
1621
  /** Rises with every new upload, so a product page can say which one it shows. */
1204
- version: z4.number().int().min(1),
1622
+ version: z5.number().int().min(1),
1205
1623
  /** The starter document this one was copied from, for information only. */
1206
- startedFrom: z4.string().trim().max(80).optional(),
1207
- inputs: z4.array(GeometryInputSchema).max(CONFIGURATOR_LIMITS.geometryInputs),
1208
- constants: z4.record(ConfiguratorNameSchema, z4.number().finite()),
1209
- slots: z4.array(GeometrySlotSchema).min(1).max(CONFIGURATOR_LIMITS.geometrySlots),
1210
- root: z4.array(geometryNodeSchema(modelFile)).min(1),
1211
- camera: z4.strictObject({
1624
+ startedFrom: z5.string().trim().max(80).optional(),
1625
+ inputs: z5.array(GeometryInputSchema).max(CONFIGURATOR_LIMITS.geometryInputs),
1626
+ constants: z5.record(ConfiguratorNameSchema, z5.number().finite()),
1627
+ slots: z5.array(GeometrySlotSchema).min(1).max(CONFIGURATOR_LIMITS.geometrySlots),
1628
+ root: z5.array(geometryNodeSchema(modelFile)).min(1),
1629
+ camera: z5.strictObject({
1212
1630
  /** Degrees around the vertical axis, 0 is straight on. */
1213
- azimuth: z4.number().min(-90).max(90),
1631
+ azimuth: z5.number().min(-90).max(90),
1214
1632
  /** Degrees above the horizon. */
1215
- elevation: z4.number().min(-10).max(80)
1633
+ elevation: z5.number().min(-10).max(80)
1216
1634
  }).optional(),
1217
- annotations: z4.array(GeometryAnnotationSchema).max(CONFIGURATOR_LIMITS.annotations)
1635
+ annotations: z5.array(GeometryAnnotationSchema).max(CONFIGURATOR_LIMITS.annotations)
1218
1636
  });
1219
1637
  }
1220
1638
  var GeometryDocumentSchema = geometryDocumentSchema(ModelPathSchema);
@@ -1504,98 +1922,98 @@ function followProblems(follows, path, context) {
1504
1922
  }
1505
1923
 
1506
1924
  // ../features/product-configurator/src/shared/configurator-structure.schema.ts
1507
- import { z as z5 } from "zod";
1508
- var LabelSchema = z5.string().trim().min(1).max(80);
1925
+ import { z as z6 } from "zod";
1926
+ var LabelSchema = z6.string().trim().min(1).max(80);
1509
1927
  var CONFIGURATOR_TEXTURE_PATH_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\.(webp|jpg|jpeg)$/;
1510
- var TexturePathSchema = z5.string().regex(CONFIGURATOR_TEXTURE_PATH_PATTERN, "configurator.invalidTexturePath");
1928
+ var TexturePathSchema = z6.string().regex(CONFIGURATOR_TEXTURE_PATH_PATTERN, "configurator.invalidTexturePath");
1511
1929
  var ParameterBase = {
1512
1930
  key: ConfiguratorNameSchema,
1513
1931
  label: LabelSchema,
1514
1932
  tab: ConfiguratorNameSchema
1515
1933
  };
1516
- var ConfiguratorParameterSchema = z5.discriminatedUnion("kind", [
1517
- z5.strictObject({
1934
+ var ConfiguratorParameterSchema = z6.discriminatedUnion("kind", [
1935
+ z6.strictObject({
1518
1936
  ...ParameterBase,
1519
- kind: z5.literal("number"),
1937
+ kind: z6.literal("number"),
1520
1938
  /** `mm` is a measure, `count` a number of things (compartments). */
1521
- unit: z5.enum(["mm", "count"]),
1522
- step: z5.number().positive().finite(),
1939
+ unit: z6.enum(["mm", "count"]),
1940
+ step: z6.number().positive().finite(),
1523
1941
  /** The widest range any model may offer; a model narrows it in its value. */
1524
- min: z5.number().finite(),
1525
- max: z5.number().finite()
1942
+ min: z6.number().finite(),
1943
+ max: z6.number().finite()
1526
1944
  }),
1527
- z5.strictObject({
1945
+ z6.strictObject({
1528
1946
  ...ParameterBase,
1529
- kind: z5.literal("choice"),
1947
+ kind: z6.literal("choice"),
1530
1948
  list: ConfiguratorNameSchema
1531
1949
  }),
1532
- z5.strictObject({ ...ParameterBase, kind: z5.literal("material") }),
1533
- z5.strictObject({ ...ParameterBase, kind: z5.literal("toggle") })
1950
+ z6.strictObject({ ...ParameterBase, kind: z6.literal("material") }),
1951
+ z6.strictObject({ ...ParameterBase, kind: z6.literal("toggle") })
1534
1952
  ]);
1535
- var choiceListSchema = (picture) => z5.strictObject({
1953
+ var choiceListSchema = (picture) => z6.strictObject({
1536
1954
  key: ConfiguratorNameSchema,
1537
1955
  label: LabelSchema,
1538
- options: z5.array(
1539
- z5.strictObject({
1956
+ options: z6.array(
1957
+ z6.strictObject({
1540
1958
  code: ConfiguratorCodeSchema,
1541
1959
  label: LabelSchema,
1542
1960
  image: picture.optional()
1543
1961
  })
1544
1962
  ).min(1).max(CONFIGURATOR_LIMITS.optionsPerList)
1545
1963
  });
1546
- var materialSchema = (picture) => z5.strictObject({
1964
+ var materialSchema = (picture) => z6.strictObject({
1547
1965
  code: ConfiguratorCodeSchema,
1548
1966
  label: LabelSchema,
1549
- color: z5.string().regex(/^#[0-9a-f]{6}$/i, "configurator.invalidColor"),
1967
+ color: z6.string().regex(/^#[0-9a-f]{6}$/i, "configurator.invalidColor"),
1550
1968
  texture: picture.optional(),
1551
1969
  /** Millimetres one repeat of the texture covers; the two maps below share it. */
1552
- textureSize: z5.number().positive().max(1e4).optional(),
1970
+ textureSize: z6.number().positive().max(1e4).optional(),
1553
1971
  /** Relief that moves with the light: a tangent-space normal map. */
1554
1972
  normalMap: picture.optional(),
1555
1973
  /** How deep the relief reads; 1 is the map as drawn. */
1556
- normalScale: z5.number().min(0).max(3).optional(),
1974
+ normalScale: z6.number().min(0).max(3).optional(),
1557
1975
  /** Where the surface is matte and where it shines (the green channel), times `roughness`. */
1558
1976
  roughnessMap: picture.optional(),
1559
- roughness: z5.number().min(0).max(1).optional(),
1560
- metalness: z5.number().min(0).max(1).optional(),
1977
+ roughness: z6.number().min(0).max(1).optional(),
1978
+ metalness: z6.number().min(0).max(1).optional(),
1561
1979
  /** How much of what is behind it shows through: glass in front of a flame. Absent is solid. */
1562
- opacity: z5.number().min(0.05).max(1).optional(),
1980
+ opacity: z6.number().min(0.05).max(1).optional(),
1563
1981
  /** Gives light rather than reflecting it: a flame, a LED. */
1564
- emissive: z5.boolean().optional(),
1982
+ emissive: z6.boolean().optional(),
1565
1983
  priceGroup: ConfiguratorCodeSchema.optional()
1566
1984
  });
1567
1985
  var ConfiguratorChoiceListSchema = choiceListSchema(TexturePathSchema);
1568
1986
  var ConfiguratorMaterialSchema = materialSchema(TexturePathSchema);
1569
- var ConditionValueSchema = z5.union([
1570
- z5.number().finite(),
1571
- z5.boolean(),
1987
+ var ConditionValueSchema = z6.union([
1988
+ z6.number().finite(),
1989
+ z6.boolean(),
1572
1990
  ConfiguratorCodeSchema,
1573
- z5.array(ConfiguratorCodeSchema).min(1).max(CONFIGURATOR_LIMITS.optionsPerList)
1991
+ z6.array(ConfiguratorCodeSchema).min(1).max(CONFIGURATOR_LIMITS.optionsPerList)
1574
1992
  ]);
1575
- var ConfiguratorConditionSchema = z5.strictObject({
1993
+ var ConfiguratorConditionSchema = z6.strictObject({
1576
1994
  param: ConfiguratorNameSchema,
1577
- op: z5.enum(["eq", "neq", "in", "gte", "lte"]),
1995
+ op: z6.enum(["eq", "neq", "in", "gte", "lte"]),
1578
1996
  value: ConditionValueSchema
1579
1997
  });
1580
- var ConfiguratorRuleSchema = z5.discriminatedUnion("kind", [
1998
+ var ConfiguratorRuleSchema = z6.discriminatedUnion("kind", [
1581
1999
  /** Show `param` only while `when` holds. */
1582
- z5.strictObject({
1583
- kind: z5.literal("visible"),
2000
+ z6.strictObject({
2001
+ kind: z6.literal("visible"),
1584
2002
  param: ConfiguratorNameSchema,
1585
2003
  when: ConfiguratorConditionSchema
1586
2004
  }),
1587
2005
  /** Narrow a number parameter: `max cabinet_height = total_height - 300`. */
1588
- z5.strictObject({
1589
- kind: z5.literal("limit"),
2006
+ z6.strictObject({
2007
+ kind: z6.literal("limit"),
1590
2008
  param: ConfiguratorNameSchema,
1591
- bound: z5.enum(["min", "max"]),
2009
+ bound: z6.enum(["min", "max"]),
1592
2010
  formula: FormulaSchema
1593
2011
  }),
1594
2012
  /** While `when` holds, these codes of a choice or material are not offered. */
1595
- z5.strictObject({
1596
- kind: z5.literal("exclude"),
2013
+ z6.strictObject({
2014
+ kind: z6.literal("exclude"),
1597
2015
  param: ConfiguratorNameSchema,
1598
- codes: z5.array(ConfiguratorCodeSchema).min(1).max(CONFIGURATOR_LIMITS.optionsPerList),
2016
+ codes: z6.array(ConfiguratorCodeSchema).min(1).max(CONFIGURATOR_LIMITS.optionsPerList),
1599
2017
  when: ConfiguratorConditionSchema
1600
2018
  })
1601
2019
  ]);
@@ -1605,23 +2023,23 @@ var PriceRuleBase = {
1605
2023
  /** How many times the amount counts, e.g. `compartments`. Absent is once. */
1606
2024
  multiplier: FormulaSchema.optional()
1607
2025
  };
1608
- var ConfiguratorPriceRuleSchema = z5.discriminatedUnion("kind", [
2026
+ var ConfiguratorPriceRuleSchema = z6.discriminatedUnion("kind", [
1609
2027
  /** The surcharge the model gives the chosen option. */
1610
- z5.strictObject({
2028
+ z6.strictObject({
1611
2029
  ...PriceRuleBase,
1612
- kind: z5.literal("choice"),
2030
+ kind: z6.literal("choice"),
1613
2031
  param: ConfiguratorNameSchema
1614
2032
  }),
1615
2033
  /** The surcharge of the chosen material's price group. */
1616
- z5.strictObject({
2034
+ z6.strictObject({
1617
2035
  ...PriceRuleBase,
1618
- kind: z5.literal("material"),
2036
+ kind: z6.literal("material"),
1619
2037
  param: ConfiguratorNameSchema
1620
2038
  }),
1621
2039
  /** An amount, counted while a switch is on. */
1622
- z5.strictObject({
2040
+ z6.strictObject({
1623
2041
  ...PriceRuleBase,
1624
- kind: z5.literal("toggle"),
2042
+ kind: z6.literal("toggle"),
1625
2043
  param: ConfiguratorNameSchema,
1626
2044
  amount: ConfiguratorNameSchema
1627
2045
  }),
@@ -1629,10 +2047,10 @@ var ConfiguratorPriceRuleSchema = z5.discriminatedUnion("kind", [
1629
2047
  * A price on measure: `max(0, total_width - 1800) / 1000 * meter_price`.
1630
2048
  * Reads number parameters (in their unit) and amounts (in cents).
1631
2049
  */
1632
- z5.strictObject({
2050
+ z6.strictObject({
1633
2051
  key: ConfiguratorNameSchema,
1634
2052
  label: LabelSchema,
1635
- kind: z5.literal("formula"),
2053
+ kind: z6.literal("formula"),
1636
2054
  formula: FormulaSchema
1637
2055
  })
1638
2056
  ]);
@@ -1642,43 +2060,43 @@ var CONFIGURATOR_SCENE_LIGHTINGS = [
1642
2060
  "daylight"
1643
2061
  ];
1644
2062
  var CONFIGURATOR_SCENE_EDGES = ["soft", "sharp"];
1645
- var ConfiguratorSceneSchema = z5.strictObject({
1646
- lighting: z5.enum(CONFIGURATOR_SCENE_LIGHTINGS).optional(),
1647
- edges: z5.enum(CONFIGURATOR_SCENE_EDGES).optional()
2063
+ var ConfiguratorSceneSchema = z6.strictObject({
2064
+ lighting: z6.enum(CONFIGURATOR_SCENE_LIGHTINGS).optional(),
2065
+ edges: z6.enum(CONFIGURATOR_SCENE_EDGES).optional()
1648
2066
  });
1649
- var structureObjectSchema = (files) => z5.strictObject({
1650
- schemaVersion: z5.literal(1),
2067
+ var structureObjectSchema = (files) => z6.strictObject({
2068
+ schemaVersion: z6.literal(1),
1651
2069
  geometry: geometryDocumentSchema(files.model),
1652
2070
  /**
1653
2071
  * How the scene is presented. Part of the structure and not of the shape
1654
2072
  * document, so the owner sets it under `crm.manage` like a material.
1655
2073
  */
1656
2074
  scene: ConfiguratorSceneSchema.optional(),
1657
- tabs: z5.array(
1658
- z5.strictObject({ key: ConfiguratorNameSchema, label: LabelSchema })
2075
+ tabs: z6.array(
2076
+ z6.strictObject({ key: ConfiguratorNameSchema, label: LabelSchema })
1659
2077
  ).min(1).max(CONFIGURATOR_LIMITS.tabs),
1660
- parameters: z5.array(ConfiguratorParameterSchema).max(CONFIGURATOR_LIMITS.parameters),
1661
- choiceLists: z5.array(choiceListSchema(files.picture)).max(CONFIGURATOR_LIMITS.choiceLists),
1662
- materials: z5.array(materialSchema(files.picture)).max(CONFIGURATOR_LIMITS.materials),
1663
- priceGroups: z5.array(
1664
- z5.strictObject({ code: ConfiguratorCodeSchema, label: LabelSchema })
2078
+ parameters: z6.array(ConfiguratorParameterSchema).max(CONFIGURATOR_LIMITS.parameters),
2079
+ choiceLists: z6.array(choiceListSchema(files.picture)).max(CONFIGURATOR_LIMITS.choiceLists),
2080
+ materials: z6.array(materialSchema(files.picture)).max(CONFIGURATOR_LIMITS.materials),
2081
+ priceGroups: z6.array(
2082
+ z6.strictObject({ code: ConfiguratorCodeSchema, label: LabelSchema })
1665
2083
  ).max(CONFIGURATOR_LIMITS.priceGroups),
1666
2084
  /** Named amounts a model fills in and price rules read (`meter_price`). */
1667
- amounts: z5.array(
1668
- z5.strictObject({ key: ConfiguratorNameSchema, label: LabelSchema })
2085
+ amounts: z6.array(
2086
+ z6.strictObject({ key: ConfiguratorNameSchema, label: LabelSchema })
1669
2087
  ).max(CONFIGURATOR_LIMITS.amounts),
1670
2088
  /**
1671
2089
  * A list of fixed combinations a model offers, each setting some number
1672
2090
  * parameters at once: a fireplace and the niche it needs.
1673
2091
  */
1674
- presets: z5.strictObject({
2092
+ presets: z6.strictObject({
1675
2093
  label: LabelSchema,
1676
2094
  tab: ConfiguratorNameSchema,
1677
- sets: z5.array(ConfiguratorNameSchema).min(1).max(CONFIGURATOR_LIMITS.parameters),
1678
- required: z5.boolean()
2095
+ sets: z6.array(ConfiguratorNameSchema).min(1).max(CONFIGURATOR_LIMITS.parameters),
2096
+ required: z6.boolean()
1679
2097
  }).optional(),
1680
- rules: z5.array(ConfiguratorRuleSchema).max(CONFIGURATOR_LIMITS.rules),
1681
- priceRules: z5.array(ConfiguratorPriceRuleSchema).max(CONFIGURATOR_LIMITS.priceRules)
2098
+ rules: z6.array(ConfiguratorRuleSchema).max(CONFIGURATOR_LIMITS.rules),
2099
+ priceRules: z6.array(ConfiguratorPriceRuleSchema).max(CONFIGURATOR_LIMITS.priceRules)
1682
2100
  });
1683
2101
  var ConfiguratorStructureObjectSchema = structureObjectSchema({
1684
2102
  picture: TexturePathSchema,
@@ -1837,8 +2255,8 @@ function configuratorStructureProblems(structure) {
1837
2255
  };
1838
2256
  structure.rules.forEach((rule, index) => {
1839
2257
  const path = ["rules", index];
1840
- const target2 = parameters.get(rule.param);
1841
- if (!target2) {
2258
+ const target3 = parameters.get(rule.param);
2259
+ if (!target3) {
1842
2260
  problems.push(
1843
2261
  issue([...path, "param"], "configurator.unknownParam", {
1844
2262
  key: rule.param
@@ -1847,7 +2265,7 @@ function configuratorStructureProblems(structure) {
1847
2265
  return;
1848
2266
  }
1849
2267
  if (rule.kind === "limit") {
1850
- if (target2.kind !== "number") {
2268
+ if (target3.kind !== "number") {
1851
2269
  problems.push(
1852
2270
  issue([...path, "param"], "configurator.notANumberParam", {
1853
2271
  key: rule.param
@@ -1865,7 +2283,7 @@ function configuratorStructureProblems(structure) {
1865
2283
  return;
1866
2284
  }
1867
2285
  if (rule.kind === "exclude") {
1868
- const codes = codesOf(target2, lists, materials);
2286
+ const codes = codesOf(target3, lists, materials);
1869
2287
  if (!codes) {
1870
2288
  problems.push(
1871
2289
  issue([...path, "param"], "configurator.notAChoiceParam", {
@@ -1944,8 +2362,8 @@ function configuratorStructureProblems(structure) {
1944
2362
  problems
1945
2363
  );
1946
2364
  }
1947
- const target2 = parameters.get(rule.param);
1948
- if (!target2 || target2.kind !== rule.kind) {
2365
+ const target3 = parameters.get(rule.param);
2366
+ if (!target3 || target3.kind !== rule.kind) {
1949
2367
  problems.push(
1950
2368
  issue([...path, "param"], "configurator.priceRuleParamKind", {
1951
2369
  key: rule.param,
@@ -2094,55 +2512,55 @@ function parameterResolutionOrder(structure) {
2094
2512
  }
2095
2513
 
2096
2514
  // ../features/product-configurator/src/shared/configurator-value.schema.ts
2097
- import { z as z6 } from "zod";
2098
- var CentsSchema = z6.number().int().min(0).max(1e8);
2515
+ import { z as z7 } from "zod";
2516
+ var CentsSchema = z7.number().int().min(0).max(1e8);
2099
2517
  var PriceSchema = CentsSchema.nullable();
2100
- var ConfiguratorValueObjectSchema = z6.strictObject({
2518
+ var ConfiguratorValueObjectSchema = z7.strictObject({
2101
2519
  /** Null: the model has no price of its own, every quote is "open". */
2102
2520
  basePriceCents: PriceSchema,
2103
- numbers: z6.record(
2521
+ numbers: z7.record(
2104
2522
  ConfiguratorNameSchema,
2105
- z6.strictObject({
2106
- min: z6.number().finite(),
2107
- max: z6.number().finite(),
2108
- default: z6.number().finite()
2523
+ z7.strictObject({
2524
+ min: z7.number().finite(),
2525
+ max: z7.number().finite(),
2526
+ default: z7.number().finite()
2109
2527
  })
2110
2528
  ).default({}),
2111
2529
  /** Per choice list: the options this model offers, with their surcharge. */
2112
- choices: z6.record(
2530
+ choices: z7.record(
2113
2531
  ConfiguratorNameSchema,
2114
- z6.strictObject({
2115
- options: z6.record(
2532
+ z7.strictObject({
2533
+ options: z7.record(
2116
2534
  ConfiguratorCodeSchema,
2117
- z6.strictObject({ priceCents: PriceSchema })
2535
+ z7.strictObject({ priceCents: PriceSchema })
2118
2536
  ),
2119
2537
  default: ConfiguratorCodeSchema.optional()
2120
2538
  })
2121
2539
  ).default({}),
2122
2540
  /** Per material parameter: the materials this model offers. */
2123
- materials: z6.record(
2541
+ materials: z7.record(
2124
2542
  ConfiguratorNameSchema,
2125
- z6.strictObject({
2126
- offered: z6.array(ConfiguratorCodeSchema).max(CONFIGURATOR_LIMITS.materials),
2543
+ z7.strictObject({
2544
+ offered: z7.array(ConfiguratorCodeSchema).max(CONFIGURATOR_LIMITS.materials),
2127
2545
  default: ConfiguratorCodeSchema.optional()
2128
2546
  })
2129
2547
  ).default({}),
2130
2548
  /** Per switch: whether this model offers it, and how it starts. Absent is offered, off. */
2131
- toggles: z6.record(
2549
+ toggles: z7.record(
2132
2550
  ConfiguratorNameSchema,
2133
- z6.strictObject({ offered: z6.boolean(), default: z6.boolean() })
2551
+ z7.strictObject({ offered: z7.boolean(), default: z7.boolean() })
2134
2552
  ).default({}),
2135
2553
  /** The surcharge of each material price group. */
2136
- priceGroups: z6.record(ConfiguratorCodeSchema, PriceSchema).default({}),
2554
+ priceGroups: z7.record(ConfiguratorCodeSchema, PriceSchema).default({}),
2137
2555
  /** The amounts the structure names, such as a price per metre. */
2138
- amounts: z6.record(ConfiguratorNameSchema, PriceSchema).default({}),
2139
- presets: z6.array(
2140
- z6.strictObject({
2556
+ amounts: z7.record(ConfiguratorNameSchema, PriceSchema).default({}),
2557
+ presets: z7.array(
2558
+ z7.strictObject({
2141
2559
  code: ConfiguratorCodeSchema,
2142
- label: z6.string().trim().min(1).max(80),
2560
+ label: z7.string().trim().min(1).max(80),
2143
2561
  /** A product of the same account this preset stands for, e.g. the fireplace. */
2144
- productId: z6.string().uuid().nullable(),
2145
- values: z6.record(ConfiguratorNameSchema, z6.number().finite())
2562
+ productId: z7.string().uuid().nullable(),
2563
+ values: z7.record(ConfiguratorNameSchema, z7.number().finite())
2146
2564
  })
2147
2565
  ).max(CONFIGURATOR_LIMITS.presets).default([])
2148
2566
  });
@@ -2601,9 +3019,9 @@ function modelOf(node, chosen) {
2601
3019
  }
2602
3020
 
2603
3021
  // src/configurator-tools.ts
2604
- import { readFile, stat } from "fs/promises";
2605
- import { extname } from "path";
2606
- var FILE_RULES = {
3022
+ import { readFile as readFile2, stat as stat2 } from "fs/promises";
3023
+ import { extname as extname2 } from "path";
3024
+ var FILE_RULES2 = {
2607
3025
  model: { extensions: [".glb"], maxBytes: 10 * 1024 * 1024 },
2608
3026
  texture: {
2609
3027
  extensions: [".webp", ".jpg", ".jpeg"],
@@ -2611,7 +3029,7 @@ var FILE_RULES = {
2611
3029
  }
2612
3030
  };
2613
3031
  var CONFIGURATOR = 'A "configurator" field holds a 3D product configurator. Its structure has "geometry" (the shape document: "inputs" bound to parameters, "constants", material "slots", a "root" tree of building blocks and "annotations" for dimension lines), "parameters", "choiceLists", "materials", "priceGroups", "amounts", optional "presets", optional "scene" ({ "lighting": "studio" | "warm" | "daylight" }), "rules" and "priceRules". Building blocks: box, rounded_top_box (params.radius), stadium_prism, arc_panel (params.thickness, params.mirror), led_strip, niche_panel (params.niche_x, niche_y, niche_width, niche_height), model (a "model" path to a .glb uploaded with upload_configurator_file, params.fit 0 contain or 1 stretch, params.paint 1 to use the slot material) and repeat (count, children, index i or j). Every measure is millimetres; "at" is the minimum corner, x left to right, y bottom to top, z from the wall into the room; "size" is width, height, depth. A measure is a number or a formula with + - * / ( ) and min, max, floor, ceil, round, abs over the shape\u2019s inputs, constants and the repeat index. Texture paths in materials come from upload_configurator_file too. Making a configurator, or changing its "geometry", needs a key with the configurator:shape scope; everything around the shape needs only catalog:write.';
2614
- var documentInput = z7.string().min(2).max(2e5).describe(
3032
+ var documentInput = z8.string().min(2).max(2e5).describe(
2615
3033
  'The JSON text of a whole configurator structure (it has "schemaVersion"), or of a shape document alone (it has "root").'
2616
3034
  );
2617
3035
  function registerConfiguratorTools(server, client) {
@@ -2643,11 +3061,11 @@ function registerConfiguratorTools(server, client) {
2643
3061
  slug: CatalogFieldSlugSchema.describe(
2644
3062
  "The slug of the configurator field."
2645
3063
  ),
2646
- label: z7.string().trim().min(1).max(120).optional(),
2647
- structure: z7.string().min(2).max(2e5).describe(
3064
+ label: z8.string().trim().min(1).max(120).optional(),
3065
+ structure: z8.string().min(2).max(2e5).describe(
2648
3066
  'The JSON text of the whole configurator structure (it has "schemaVersion").'
2649
3067
  ),
2650
- public: z7.boolean().optional().describe("Whether product pages may show the configurator."),
3068
+ public: z8.boolean().optional().describe("Whether product pages may show the configurator."),
2651
3069
  ...objectInput
2652
3070
  },
2653
3071
  annotations: { destructiveHint: true, idempotentHint: true }
@@ -2697,21 +3115,21 @@ function registerConfiguratorTools(server, client) {
2697
3115
  fieldSlug: CatalogFieldSlugSchema.describe(
2698
3116
  "The slug of the configurator field."
2699
3117
  ),
2700
- kind: z7.enum(CONFIGURATOR_FILE_KINDS),
2701
- filePath: z7.string().min(1).max(1e3).describe("The absolute path of the file on this machine."),
3118
+ kind: z8.enum(CONFIGURATOR_FILE_KINDS),
3119
+ filePath: z8.string().min(1).max(1e3).describe("The absolute path of the file on this machine."),
2702
3120
  ...objectInput
2703
3121
  },
2704
3122
  annotations: { destructiveHint: false, idempotentHint: false }
2705
3123
  },
2706
3124
  async ({ fieldSlug, kind, filePath, object: object2 }) => {
2707
- const rules = FILE_RULES[kind];
2708
- const extension = extname(filePath).toLowerCase();
3125
+ const rules = FILE_RULES2[kind];
3126
+ const extension = extname2(filePath).toLowerCase();
2709
3127
  if (!rules.extensions.includes(extension)) {
2710
3128
  return notUploaded(
2711
3129
  `A ${kind} is a ${rules.extensions.join(" or ")} file; this is "${extension || "no extension"}".`
2712
3130
  );
2713
3131
  }
2714
- const info = await stat(filePath).catch(() => null);
3132
+ const info = await stat2(filePath).catch(() => null);
2715
3133
  if (!info?.isFile()) {
2716
3134
  return notUploaded(`"${filePath}" is not a file on this machine.`);
2717
3135
  }
@@ -2721,7 +3139,7 @@ function registerConfiguratorTools(server, client) {
2721
3139
  );
2722
3140
  }
2723
3141
  try {
2724
- const bytes = new Uint8Array(await readFile(filePath));
3142
+ const bytes = new Uint8Array(await readFile2(filePath));
2725
3143
  const result = await client.uploadConfiguratorFile(
2726
3144
  fieldSlug,
2727
3145
  {
@@ -2751,8 +3169,8 @@ function registerConfiguratorTools(server, client) {
2751
3169
  title: "Read the configurator of a product",
2752
3170
  description: "The configurator field of one product as its page receives it: the structure (texture paths already URLs), the model\u2019s value (ranges, offered options, amounts, presets), the tax a quote uses and the two hashes a request must send back. Useful to draft a new shape from an existing one. " + CONFIGURATOR,
2753
3171
  inputSchema: {
2754
- slugOrId: z7.string().min(1).max(200).describe("The address (slug) or the id of the product."),
2755
- field: z7.string().min(1).max(64).describe("The slug of the configurator field.")
3172
+ slugOrId: z8.string().min(1).max(200).describe("The address (slug) or the id of the product."),
3173
+ field: z8.string().min(1).max(64).describe("The slug of the configurator field.")
2756
3174
  },
2757
3175
  annotations: { readOnlyHint: true, idempotentHint: true }
2758
3176
  },
@@ -2834,8 +3252,8 @@ function isRecord(value) {
2834
3252
  }
2835
3253
 
2836
3254
  // src/event-tools.ts
2837
- import { z as z8 } from "zod";
2838
- var moment = z8.string().datetime({ offset: true }).optional().describe("An ISO timestamp with its offset, such as 2026-11-01T00:00:00Z.");
3255
+ import { z as z9 } from "zod";
3256
+ var moment = z9.string().datetime({ offset: true }).optional().describe("An ISO timestamp with its offset, such as 2026-11-01T00:00:00Z.");
2839
3257
  function registerEventTools(server, client) {
2840
3258
  server.registerTool(
2841
3259
  "list_events",
@@ -2863,7 +3281,7 @@ function registerEventTools(server, client) {
2863
3281
  title: "Read one event",
2864
3282
  description: "One event as its page needs it: description, doors and sales times, address, ticket types with their prices, and the texts for search engines. May be a minute old; ask get_event_availability for seats that must be current.",
2865
3283
  inputSchema: {
2866
- idOrSlug: z8.string().min(1).max(200).describe("The id or the address (slug) of the event.")
3284
+ idOrSlug: z9.string().min(1).max(200).describe("The id or the address (slug) of the event.")
2867
3285
  },
2868
3286
  annotations: { readOnlyHint: true, idempotentHint: true }
2869
3287
  },
@@ -2888,7 +3306,7 @@ function registerEventTools(server, client) {
2888
3306
  title: "Seats left for an event",
2889
3307
  description: "Live, not cached: how many seats are left for the event and for each ticket type, and whether it is sold out. A remaining of null means there is no ceiling.",
2890
3308
  inputSchema: {
2891
- eventId: z8.string().uuid().describe("The id of the event.")
3309
+ eventId: z9.string().uuid().describe("The id of the event.")
2892
3310
  },
2893
3311
  annotations: { readOnlyHint: true, idempotentHint: true }
2894
3312
  },
@@ -2916,9 +3334,9 @@ function describeEvent(event) {
2916
3334
  }
2917
3335
 
2918
3336
  // src/placement-tools.ts
2919
- import { z as z9 } from "zod";
3337
+ import { z as z10 } from "zod";
2920
3338
  var REACT_HINT = "On a React or Next.js site, prefer the components in `@solumflow-app/crm-client/react` (CrmForm, CrmBooking, CrmChatWidget, CrmSiteTracking) over pasting HTML.";
2921
- var origin = z9.string().regex(/^https?:\/\/[a-zA-Z0-9.-]+(:[0-9]{1,5})?$/).describe("Scheme, host and optional port; no path: https://www.example.com");
3339
+ var origin = z10.string().regex(/^https?:\/\/[a-zA-Z0-9.-]+(:[0-9]{1,5})?$/).describe("Scheme, host and optional port; no path: https://www.example.com");
2922
3340
  function registerPlacementTools(server, client) {
2923
3341
  server.registerTool(
2924
3342
  "list_forms",
@@ -2991,13 +3409,13 @@ function registerPlacementTools(server, client) {
2991
3409
  title: "Make or change a chat widget",
2992
3410
  description: "Makes the chat widget at this address when there is none, or changes it. Sets its name, whether it is live, and which sites it may run on. How it looks and what it says is done in the app\u2019s builder. Sites are added and removed, never replaced. Careful: an empty list means ANY site, so adding the first origin narrows the widget to that site only, and removing an origin takes the chat off a site that may be live.",
2993
3411
  inputSchema: {
2994
- slug: z9.string().regex(/^[a-z][a-z0-9-]*$/).max(100).describe(
3412
+ slug: z10.string().regex(/^[a-z][a-z0-9-]*$/).max(100).describe(
2995
3413
  'The widget\u2019s address on this account, such as "website-chat".'
2996
3414
  ),
2997
- name: z9.string().min(1).max(200).optional().describe("Required when the widget does not exist yet."),
2998
- status: z9.enum(["draft", "published"]).optional().describe("Only a published widget answers on a site."),
2999
- addAllowedOrigins: z9.array(origin).max(20).optional(),
3000
- removeAllowedOrigins: z9.array(origin).max(20).optional()
3415
+ name: z10.string().min(1).max(200).optional().describe("Required when the widget does not exist yet."),
3416
+ status: z10.enum(["draft", "published"]).optional().describe("Only a published widget answers on a site."),
3417
+ addAllowedOrigins: z10.array(origin).max(20).optional(),
3418
+ removeAllowedOrigins: z10.array(origin).max(20).optional()
3001
3419
  },
3002
3420
  annotations: {
3003
3421
  readOnlyHint: false,
@@ -3045,9 +3463,9 @@ ${describeChatWidget(widget)}`,
3045
3463
  title: "Set up or change the tracking script",
3046
3464
  description: "Sets website tracking up when it is not there, adds or removes the hosts it counts visits from, and changes the consent it waits for. An empty list of hosts counts nothing, so add the host of every site the script is placed on. Hosts are added and removed, never replaced.",
3047
3465
  inputSchema: {
3048
- addAllowedHosts: z9.array(z9.string()).max(20).optional().describe('Hosts such as "www.example.com" or "*.example.com".'),
3049
- removeAllowedHosts: z9.array(z9.string()).max(20).optional(),
3050
- consentSignal: z9.enum([
3466
+ addAllowedHosts: z10.array(z10.string()).max(20).optional().describe('Hosts such as "www.example.com" or "*.example.com".'),
3467
+ removeAllowedHosts: z10.array(z10.string()).max(20).optional(),
3468
+ consentSignal: z10.enum([
3051
3469
  "ad_storage",
3052
3470
  "analytics_storage",
3053
3471
  "personalization_storage",
@@ -3152,6 +3570,7 @@ async function main() {
3152
3570
  registerPlacementTools(server, client);
3153
3571
  registerCatalogTools(server, client);
3154
3572
  registerCatalogWriteTools(server, client);
3573
+ registerCatalogMigrationTools(server, client);
3155
3574
  registerEventTools(server, client);
3156
3575
  registerConfiguratorTools(server, client);
3157
3576
  await server.connect(new StdioServerTransport());