sbuilder-mcp 0.69.0 → 0.71.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/CHANGELOG.md CHANGED
@@ -6,6 +6,24 @@ All notable changes to this project are documented in this file.
6
6
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
7
7
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
8
8
 
9
+ ## [0.71.0] - 2026-09-28
10
+
11
+ ### Added
12
+ - `sb_review`'s `catalogue` readiness gap now reports "N products and none active" separately from an empty catalogue, for a store whose products were all created without `status` and stored as drafts.
13
+ - `sb_api_find`'s `product_traps` gains a `status` trap: a product created without `status` is stored as "draft" — the storefront never lists it and the checkout refuses it as a line — even though the write still answers 201.
14
+ - The translation call sheet (via `sb_api_find`) gains `review_gate`, explaining that `PUT /api/v1/translations` defaults a row's `source` to "machine", which is stored and counted but never shown on the storefront, in search, in slug routing, or in shopper mail until `POST /api/sites/{siteId}/translations/review` (or its `/bulk` form) approves it.
15
+
16
+ ### Changed
17
+ - The generated catalog was regenerated against the platform (`web_builder` `84f1f10bc`): flash sale campaign listing operations gain query parameters `q`, `status`, `limit`, and `offset`; the order/checkout request shapes now catalog a `locale` field that snapshots the shopper's storefront language at checkout; `slug` is now catalogued as a translatable field on products, categories, articles, blog categories, courses, and pages; and `review` is no longer a translatable entity type.
18
+
19
+ ## [0.70.0] - 2026-09-26
20
+
21
+ ### Added
22
+ - `sb_review` warns once per site (`cartDrawerThumbnail`) when a cart drawer seeded before the platform fixed its seed renders a line's thumbnail as a gallery (feature image plus a thumbs strip) squeezed into a 64px line, and `sb_store` action `cart` with `relocalize: true` now also fixes it, setting that thumbnail to a single image through the open page's save.
23
+
24
+ ### Changed
25
+ - `sb_store` action `cart`'s `relocalize` no longer requires `settings.locale` to be set when the only thing left to fix is a gallery thumbnail; it still refuses when there is neither a locale word nor a thumbnail to relocalize.
26
+
9
27
  ## [0.69.0] - 2026-09-26
10
28
 
11
29
  ### Added
package/CHANGELOG.vi.md CHANGED
@@ -6,6 +6,24 @@ Mọi thay đổi đáng chú ý của dự án được ghi lại trong file n
6
6
  Định dạng dựa trên [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
7
7
  và dự án tuân theo [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
8
8
 
9
+ ## [0.71.0] - 2026-09-28
10
+
11
+ ### Added
12
+ - Gap readiness `catalogue` của `sb_review` giờ báo riêng "có N sản phẩm, không cái nào active" thay vì gộp chung với catalogue rỗng, dành cho store có toàn bộ sản phẩm được tạo mà không kèm `status` nên bị lưu thành draft.
13
+ - `product_traps` của `sb_api_find` có thêm bẫy `status`: một sản phẩm tạo không kèm `status` sẽ bị lưu thành "draft" — storefront không liệt kê và checkout từ chối nó như một dòng hàng — dù request ghi vẫn trả về 201.
14
+ - Call sheet bản dịch (qua `sb_api_find`) có thêm `review_gate`, giải thích rằng `PUT /api/v1/translations` mặc định `source` của một dòng là "machine", dòng này được lưu và tính vào tiến độ nhưng không hiện ở storefront, trong tìm kiếm, trong slug routing hay trong mail gửi khách cho tới khi `POST /api/sites/{siteId}/translations/review` (hoặc bản `/bulk`) duyệt nó.
15
+
16
+ ### Changed
17
+ - Catalog được sinh ra đã được làm mới dựa trên nền tảng (`web_builder` `84f1f10bc`): các operation liệt kê flash sale campaign có thêm tham số truy vấn `q`, `status`, `limit`, `offset`; request shape của order/checkout giờ catalog thêm field `locale` lưu lại ngôn ngữ storefront của khách tại thời điểm checkout; `slug` giờ được catalog là field dịch được trên product, category, article, blog category, course và page; và `review` không còn là một entity type dịch được nữa.
18
+
19
+ ## [0.70.0] - 2026-09-26
20
+
21
+ ### Added
22
+ - `sb_review` cảnh báo một lần cho mỗi site (`cartDrawerThumbnail`) khi cart drawer được gieo trước khi nền tảng sửa lại seed hiện thumbnail của một line dưới dạng gallery (ảnh chính cộng dải ảnh thu nhỏ) bị nhồi vào một line cao 64px, và action `cart` của `sb_store` với `relocalize: true` giờ cũng sửa luôn, đặt thumbnail đó thành một ảnh duy nhất thông qua lần lưu của trang đang mở.
23
+
24
+ ### Changed
25
+ - `relocalize` của action `cart` trong `sb_store` không còn yêu cầu `settings.locale` phải được đặt khi thứ duy nhất cần sửa là thumbnail dạng gallery; nó vẫn từ chối khi không có cả từ ngôn ngữ lẫn thumbnail nào cần relocalize.
26
+
9
27
  ## [0.69.0] - 2026-09-26
10
28
 
11
29
  ### Added
@@ -8050,6 +8050,34 @@ export const API_OPERATIONS = [
8050
8050
  "type": "string",
8051
8051
  "description": "Site ID"
8052
8052
  },
8053
+ {
8054
+ "name": "q",
8055
+ "in": "query",
8056
+ "required": false,
8057
+ "type": "string",
8058
+ "description": "Case-insensitive substring of the campaign name (GET only)"
8059
+ },
8060
+ {
8061
+ "name": "status",
8062
+ "in": "query",
8063
+ "required": false,
8064
+ "type": "string",
8065
+ "description": "off|scheduled|running|ended — derived status, evaluated now (GET only)"
8066
+ },
8067
+ {
8068
+ "name": "limit",
8069
+ "in": "query",
8070
+ "required": false,
8071
+ "type": "integer",
8072
+ "description": "Page size, default 50, max 200 (GET only)"
8073
+ },
8074
+ {
8075
+ "name": "offset",
8076
+ "in": "query",
8077
+ "required": false,
8078
+ "type": "integer",
8079
+ "description": "Page offset, default 0 (GET only)"
8080
+ },
8053
8081
  {
8054
8082
  "name": "flashSale",
8055
8083
  "in": "body",
@@ -8078,6 +8106,34 @@ export const API_OPERATIONS = [
8078
8106
  "type": "string",
8079
8107
  "description": "Site ID"
8080
8108
  },
8109
+ {
8110
+ "name": "q",
8111
+ "in": "query",
8112
+ "required": false,
8113
+ "type": "string",
8114
+ "description": "Case-insensitive substring of the campaign name (GET only)"
8115
+ },
8116
+ {
8117
+ "name": "status",
8118
+ "in": "query",
8119
+ "required": false,
8120
+ "type": "string",
8121
+ "description": "off|scheduled|running|ended — derived status, evaluated now (GET only)"
8122
+ },
8123
+ {
8124
+ "name": "limit",
8125
+ "in": "query",
8126
+ "required": false,
8127
+ "type": "integer",
8128
+ "description": "Page size, default 50, max 200 (GET only)"
8129
+ },
8130
+ {
8131
+ "name": "offset",
8132
+ "in": "query",
8133
+ "required": false,
8134
+ "type": "integer",
8135
+ "description": "Page offset, default 0 (GET only)"
8136
+ },
8081
8137
  {
8082
8138
  "name": "flashSale",
8083
8139
  "in": "body",
@@ -18416,6 +18472,10 @@ export const API_DEFINITIONS = {
18416
18472
  "$ref": "#/definitions/github_com_webbuilder_server_internal_orders.OrderItem"
18417
18473
  }
18418
18474
  },
18475
+ "locale": {
18476
+ "description": "Locale is the language the SHOPPER was reading the storefront in at\ncheckout — a SNAPSHOT, for the same reason every other checkout-time fact\non this struct is one: a merchant who later reorders or trims the site's\nlanguage list must not have an already-placed order start mailing its\nbuyer in a different language than the one they bought in.\n\n\"\" means the site's DEFAULT locale — every order placed before this field\nexisted, every order typed by staff, and every order placed through a\nsurface that has not been taught to send one yet. It is never trusted\noutright: Checkout.Create normalises it against the site's published\nlocales (SiteLocales port) before it is stored, so a stale or tampered\nclaim can only ever fall back to \"\", never choose an unpublished\nlanguage. See Checkout.normalizeLocale.",
18477
+ "type": "string"
18478
+ },
18419
18479
  "note": {
18420
18480
  "type": "string"
18421
18481
  },
@@ -1,6 +1,6 @@
1
1
  export const DEAD_KEY_SOURCE = {
2
- "files": 4486,
3
- "identifiers": 84432,
2
+ "files": 4599,
3
+ "identifiers": 85994,
4
4
  "seededKeys": 442,
5
5
  "dead": 0
6
6
  };
@@ -352,12 +352,19 @@ export function describeOperation(op) {
352
352
  '/apps/blocks again for what it contributed.',
353
353
  };
354
354
  }
355
- // FILLING A CATALOGUE WAS REACHABLE AND FIVE FACTS ABOUT IT WERE NOT, each
355
+ // FILLING A CATALOGUE WAS REACHABLE AND SIX FACTS ABOUT IT WERE NOT, each
356
356
  // failing silently. Attached to the call sheet for the same reason the
357
357
  // translation table is: this is where the agent is when it decides what to
358
358
  // send, on both credential surfaces a product can be created or replaced on.
359
359
  if (PRODUCT_WRITE_IDS.has(op.id)) {
360
360
  out.product_traps = {
361
+ // THE STATUS NOBODY SENDS. An empty Status becomes ProductStatusDraft
362
+ // (products/product.go), the storefront lists only "active", and since
363
+ // web_builder b4d60a862 the checkout REFUSES any other status as a line.
364
+ // The write answers 201 either way.
365
+ status: 'Send status: "active". A product created without one is stored as "draft" — the ' +
366
+ 'storefront never lists it and the checkout refuses it — and the write still ' +
367
+ 'answers 201. PATCH is 405: to activate later, PUT the whole object back.',
361
368
  price: 'Price lives on the VARIANT, not the product — variants[].priceCents. A product ' +
362
369
  'posted with no variant renders a catalogue entry nobody can buy.',
363
370
  // MINOR UNITS, SAID WHERE THE NUMBER IS BEING WRITTEN.
@@ -4,7 +4,7 @@ export const SHAPE_SOURCE = {
4
4
  "fromHandlers": 197,
5
5
  "fromSwaggerOnly": 0,
6
6
  "withReadOnly": 28,
7
- "structsRead": 1931
7
+ "structsRead": 1977
8
8
  };
9
9
  export const REQUEST_SHAPES = {
10
10
  "post:/api/sites/{id}/ai-credits/purchase": {
@@ -4736,6 +4736,11 @@ export const REQUEST_SHAPES = {
4736
4736
  "name": "currency",
4737
4737
  "type": "string"
4738
4738
  },
4739
+ {
4740
+ "name": "locale",
4741
+ "type": "string",
4742
+ "note": "Locale is the language the SHOPPER was reading the storefront in at checkout — a SNAPSHOT, for the same reason every other checkout-time fact on this struct is one: a merchant who later reorders or trims the site's language list must not…"
4743
+ },
4739
4744
  {
4740
4745
  "name": "discountCode",
4741
4746
  "type": "string",
@@ -5047,6 +5052,11 @@ export const REQUEST_SHAPES = {
5047
5052
  "name": "currency",
5048
5053
  "type": "string"
5049
5054
  },
5055
+ {
5056
+ "name": "locale",
5057
+ "type": "string",
5058
+ "note": "Locale is the language the SHOPPER was reading the storefront in at checkout — a SNAPSHOT, for the same reason every other checkout-time fact on this struct is one: a merchant who later reorders or trims the site's language list must not…"
5059
+ },
5050
5060
  {
5051
5061
  "name": "discountCode",
5052
5062
  "type": "string",
@@ -1,7 +1,7 @@
1
1
  // GENERATED by scripts/gen-catalog.ts — do not edit by hand.
2
2
  // The platform commit every other generated file here describes.
3
3
  export const PLATFORM_SOURCE = {
4
- "commit": "a3a7352834d9d774f5a8162663fe8b27a0422d22",
5
- "committedAt": "2026-09-26T11:21:05+07:00",
4
+ "commit": "84f1f10bcbef0748d0b18e19283ccb0458599c1b",
5
+ "committedAt": "2026-09-26T21:25:06+07:00",
6
6
  "dirty": false
7
7
  };
@@ -4,7 +4,7 @@ export const TRANSLATION_SOURCE = {
4
4
  "elements": 66,
5
5
  "pairs": 180,
6
6
  "neverKeys": 178,
7
- "entityTypes": 12
7
+ "entityTypes": 11
8
8
  };
9
9
  /** Every entity type a translation record can name, including "node". */
10
10
  export const TRANSLATION_ENTITY_TYPES = [
@@ -13,7 +13,6 @@ export const TRANSLATION_ENTITY_TYPES = [
13
13
  "article",
14
14
  "blogCategory",
15
15
  "course",
16
- "review",
17
16
  "courseSection",
18
17
  "courseLesson",
19
18
  "node",
@@ -546,6 +545,9 @@ export const TRANSLATABLE_ENTITY_FIELDS = {
546
545
  "key": "attributes",
547
546
  "list": "attributes"
548
547
  },
548
+ {
549
+ "key": "slug"
550
+ },
549
551
  {
550
552
  "key": "seoTitle"
551
553
  },
@@ -570,6 +572,9 @@ export const TRANSLATABLE_ENTITY_FIELDS = {
570
572
  "key": "description",
571
573
  "html": true
572
574
  },
575
+ {
576
+ "key": "slug"
577
+ },
573
578
  {
574
579
  "key": "seoTitle"
575
580
  },
@@ -601,6 +606,9 @@ export const TRANSLATABLE_ENTITY_FIELDS = {
601
606
  {
602
607
  "key": "tags"
603
608
  },
609
+ {
610
+ "key": "slug"
611
+ },
604
612
  {
605
613
  "key": "seoTitle"
606
614
  },
@@ -624,6 +632,9 @@ export const TRANSLATABLE_ENTITY_FIELDS = {
624
632
  {
625
633
  "key": "description"
626
634
  },
635
+ {
636
+ "key": "slug"
637
+ },
627
638
  {
628
639
  "key": "seoTitle"
629
640
  },
@@ -666,6 +677,9 @@ export const TRANSLATABLE_ENTITY_FIELDS = {
666
677
  {
667
678
  "key": "tags"
668
679
  },
680
+ {
681
+ "key": "slug"
682
+ },
669
683
  {
670
684
  "key": "seoTitle"
671
685
  },
@@ -682,12 +696,6 @@ export const TRANSLATABLE_ENTITY_FIELDS = {
682
696
  "key": "ogDesc"
683
697
  }
684
698
  ],
685
- "review": [
686
- {
687
- "key": "reply",
688
- "html": true
689
- }
690
- ],
691
699
  "courseSection": [
692
700
  {
693
701
  "key": "title"
@@ -713,6 +721,9 @@ export const TRANSLATABLE_ENTITY_FIELDS = {
713
721
  },
714
722
  {
715
723
  "key": "ogDesc"
724
+ },
725
+ {
726
+ "key": "slug"
716
727
  }
717
728
  ],
718
729
  "uiString": [
@@ -56,3 +56,18 @@ export function cartRelocalize(doc, rootId, locale) {
56
56
  });
57
57
  return out;
58
58
  }
59
+ /**
60
+ * A CART LINE THUMBNAIL SEEDED AS A GALLERY. Drawers seeded before
61
+ * web_builder `3e5efbb52` carry a childless `media-dataset` with no `layout`,
62
+ * which renders the feature image PLUS a thumbs strip inside a 64px line. The
63
+ * seed now says `single`; a layout the merchant chose explicitly is theirs.
64
+ */
65
+ export function cartGalleryThumbs(doc, rootId) {
66
+ const out = [];
67
+ walk(doc, rootId, (n) => {
68
+ const layout = n.config?.layout;
69
+ if (n.data.type === 'media-dataset' && !(n.data.nodes ?? []).length && (layout === undefined || layout === 'bottom'))
70
+ out.push(n.id);
71
+ });
72
+ return out;
73
+ }
@@ -104,6 +104,8 @@ export async function gatherReadiness(ctx, siteId, pageNodes, openPageId) {
104
104
  ? {
105
105
  active: rows.filter((p) => (p.status ?? 'active') === 'active').length,
106
106
  purchasable: rows.filter((p) => (p.status ?? 'active') === 'active' && (p.priceCents ?? 0) > 0).length,
107
+ // Drafts and archived: what a catalogue built without status:"active" is.
108
+ inactive: rows.filter((p) => (p.status ?? 'active') !== 'active').length,
107
109
  }
108
110
  : null;
109
111
  // A page an entity links to is that entity's own; only the shared template
@@ -16,7 +16,7 @@
16
16
  * fetched reports nothing rather than a gap the store may not have. A warning
17
17
  * that fires on a correctly built site is one the reader learns to ignore.
18
18
  */
19
- import { cartRelocalize, cartSeedLocale } from './cartlang.js';
19
+ import { cartGalleryThumbs, cartRelocalize, cartSeedLocale } from './cartlang.js';
20
20
  /** The purchase binding an Add-to-cart / Buy-now button carries. */
21
21
  const PRODUCT_ACTION_BINDING_ID = 'bind-product-action';
22
22
  /** Element types that only make sense in a store. */
@@ -280,6 +280,15 @@ export function readinessGaps(input) {
280
280
  });
281
281
  }
282
282
  }
283
+ if (input.cartOverlay?.root_node_id && cartGalleryThumbs(input.cartOverlay, input.cartOverlay.root_node_id).length) {
284
+ gaps.push({
285
+ id: 'cartDrawerThumbnail',
286
+ draft: false,
287
+ problem: 'The cart drawer\'s line thumbnail is a GALLERY (feature image plus a thumbs strip) squeezed ' +
288
+ 'into a 64px line — a drawer seeded before the platform fixed its seed.',
289
+ fix: 'Open any page of this site, then sb_store action:"cart" relocalize:true dry_run:false — it sets that thumbnail to a single image.',
290
+ });
291
+ }
283
292
  if (!isStore(input))
284
293
  return gaps;
285
294
  if (pages && !published(pages, 'checkout')) {
@@ -354,14 +363,31 @@ export function readinessGaps(input) {
354
363
  // NOTHING TO SELL. Checked before the delivery option, because a shipping
355
364
  // method for an empty catalogue is furniture.
356
365
  if (input.products) {
357
- if (input.products.active === 0) {
366
+ if (input.products.active === 0 && (input.products.inactive ?? 0) > 0) {
367
+ // PRODUCTS EXIST AND NONE IS ACTIVE. A product created without a status is
368
+ // stored as "draft" (products.go: an empty Status becomes ProductStatusDraft),
369
+ // the storefront lists only "active", and since web_builder b4d60a862 the
370
+ // checkout refuses any other status outright — so a catalogue built through
371
+ // the API without saying status:"active" is a store with nothing to buy.
372
+ gaps.push({
373
+ id: 'catalogue',
374
+ draft: false,
375
+ problem: `The store has ${input.products.inactive} products and none is active — a product ` +
376
+ 'created without status is stored as "draft", which the storefront never lists and ' +
377
+ 'the checkout refuses. Every product list renders its empty state.',
378
+ fix: 'PUT /api/v1/products/{id} with the whole object read back and status: "active" ' +
379
+ '(PATCH is 405). Send status: "active" on every POST /api/v1/products from now on.',
380
+ });
381
+ }
382
+ else if (input.products.active === 0) {
358
383
  gaps.push({
359
384
  id: 'catalogue',
360
385
  draft: false,
361
386
  problem: 'The store has no active products. Every product list on the site renders its empty ' +
362
387
  'state, the product template has nothing to bind to, and there is nothing to add to a ' +
363
388
  'cart — on a site that otherwise reports ready.',
364
- fix: 'Create products (sb_api_find "create product"). priceCents is minor units — VND × 100.',
389
+ fix: 'Create products (sb_api_find "create product") with status: "active" — one created ' +
390
+ 'without a status is a draft. priceCents is minor units — VND × 100.',
365
391
  });
366
392
  }
367
393
  else if (input.products.purchasable === 0) {
@@ -68,5 +68,17 @@ export function translationCallSheet() {
68
68
  'that element allows. Translating any OTHER special BREAKS the render rather than ' +
69
69
  'degrading it: `name` is a lucide icon id, `src` a URL, `filterSource` a registry id the ' +
70
70
  'renderer switches on. Never walk a document translating every string you find.',
71
+ // THE REVIEW GATE (web_builder c6184c085). The storefront, search, slug routing
72
+ // and shopper mail read only rows whose source is "human". The key surface
73
+ // defaults source to "machine" — deliberately, so an app cannot mislabel an
74
+ // unread value as reviewed — so a translation written there is stored,
75
+ // counted by /progress, and never SHOWN until a review call approves it.
76
+ review_gate: 'Only rows with source "human" are served — on the storefront, in search, in slug ' +
77
+ 'routing and in shopper mail. PUT /api/v1/translations defaults source to "machine": ' +
78
+ 'the row is stored and counted by GET /api/v1/translations/progress, and shown to ' +
79
+ 'nobody until POST /api/sites/{siteId}/translations/review {locale, entityType, ' +
80
+ 'entityId, field} or …/review/bulk approves it (session credential). Send ' +
81
+ '"source": "human" only when a person read the text; the site-scoped PUT ' +
82
+ '/api/sites/{siteId}/translations already defaults to "human".',
71
83
  };
72
84
  }
@@ -57,7 +57,7 @@ import { siteToken } from './credentialpick.js';
57
57
  import { setKeys } from '../domains/site/builder.js';
58
58
  import { CART_SEEDS, OVERLAY_SEEDS } from '../catalog/overlays.generated.js';
59
59
  import { withFreshIds } from '../domains/site/ids.js';
60
- import { cartRelocalize, cartSeedLocale } from '../domains/site/cartlang.js';
60
+ import { cartGalleryThumbs, cartRelocalize, cartSeedLocale } from '../domains/site/cartlang.js';
61
61
  const KIND_NAME = {
62
62
  popup: 'Pop-up',
63
63
  quickview: 'Quick view',
@@ -363,29 +363,34 @@ export async function relocalizeCartDrawer(ctx, session, siteId, dryRun) {
363
363
  throw new Error('sbuilder: this site has no cart drawer — sb_store action:"cart" without relocalize creates one.');
364
364
  const settings = (await get(`/api/sites/${site}/settings`));
365
365
  const raw = settings.settings?.locale;
366
- if (typeof raw !== 'string' || !raw) {
367
- throw new Error('sbuilder: settings.locale is unset, so there is no site language to relocalize to — set it with sb_theme locale first.');
368
- }
369
- const language = cartSeedLocale(raw);
366
+ // No locale means no language to move the WORDS to; the thumbnail needs none.
367
+ const language = typeof raw === 'string' && raw ? cartSeedLocale(raw) : null;
370
368
  const open = session.peek() ? session.location() : null;
371
369
  const composed = open?.siteId === siteId
372
370
  ? Object.values(session.current().doc.nodes).find((n) => n.specials?.overlayId === cart.id)
373
371
  : undefined;
374
- const changes = composed
375
- ? cartRelocalize(session.current().doc, composed.id, language)
372
+ const target = composed
373
+ ? { doc: session.current().doc, root: composed.id }
376
374
  : cart.document?.root_node_id
377
- ? cartRelocalize(cart.document, cart.document.root_node_id, language)
378
- : [];
379
- if (dryRun || changes.length === 0) {
375
+ ? { doc: cart.document, root: cart.document.root_node_id }
376
+ : null;
377
+ const changes = target && language ? cartRelocalize(target.doc, target.root, language) : [];
378
+ const thumbnails = target ? cartGalleryThumbs(target.doc, target.root) : [];
379
+ if (!language && thumbnails.length === 0) {
380
+ throw new Error('sbuilder: settings.locale is unset, so there is no site language to relocalize to — set it with sb_theme locale first.');
381
+ }
382
+ const thumbNote = thumbnails.length ? { thumbnails, thumbnails_note: 'These line thumbnails become a single image (config.layout "single").' } : {};
383
+ if (dryRun || changes.length + thumbnails.length === 0) {
380
384
  return {
381
385
  ...(dryRun ? { dry_run: true } : {}),
382
386
  overlay_id: cart.id,
383
387
  language,
384
388
  changes,
385
- ...(changes.length === 0
386
- ? { note: 'Nothing to change — no seed word from another language is left in the drawer.' }
389
+ ...thumbNote,
390
+ ...(changes.length + thumbnails.length === 0
391
+ ? { note: 'Nothing to change — no seed word from another language and no gallery thumbnail is left in the drawer.' }
387
392
  : {
388
- plan: redact([{ step: 1, what: 'rewrite those strings in the composed drawer and save the open page', method: 'PUT', path: `/api/sites/${site}/pages/{open page}/source` }]),
393
+ plan: redact([{ step: 1, what: 'apply those changes to the composed drawer and save the open page', method: 'PUT', path: `/api/sites/${site}/pages/{open page}/source` }]),
389
394
  note: 'Nothing was sent. Re-call with dry_run:false; it writes through the open page\'s save.',
390
395
  }),
391
396
  };
@@ -393,11 +398,15 @@ export async function relocalizeCartDrawer(ctx, session, siteId, dryRun) {
393
398
  if (!composed) {
394
399
  throw new Error(`sbuilder: the drawer is written through a page save, so relocalize needs a page of site "${siteId}" open — sb_page_open one, then re-run.`);
395
400
  }
396
- await session.applyAndSave(changes.map((c) => ({ op: 'set', path: ['nodes', c.node_id, 'specials', c.key], value: c.to })));
401
+ await session.applyAndSave([
402
+ ...changes.map((c) => ({ op: 'set', path: ['nodes', c.node_id, 'specials', c.key], value: c.to })),
403
+ ...thumbnails.map((id) => ({ op: 'set', path: ['nodes', id, 'config', 'layout'], value: 'single' })),
404
+ ]);
397
405
  return {
398
406
  overlay_id: cart.id,
399
407
  language,
400
408
  changes,
409
+ ...thumbNote,
401
410
  next: 'Publish the pages: the drawer reaches a shopper through each published page.',
402
411
  };
403
412
  }
@@ -1,7 +1,7 @@
1
1
  /**
2
2
  * THE STORE FLOWS THAT MUST BE ORDERED.
3
3
  *
4
- * `sb_review` names EIGHTEEN readiness gaps (`ReadinessGapId`). Most are one
4
+ * `sb_review` names its readiness gaps (`ReadinessGapId`). Most are one
5
5
  * call each — a delivery option, a payment gateway, a product, a page of the
6
6
  * right type — because `REQUEST_SHAPES` tells the caller what those calls take.
7
7
  * The ones that are not are the flows in this file: each is several writes in
@@ -533,7 +533,8 @@ export function registerStoreTools(server, ctx, session) {
533
533
  .boolean()
534
534
  .optional()
535
535
  .describe('action:"cart" — rewrite an EXISTING drawer\'s seed words that are in another language ' +
536
- 'to the site locale\'s (edited text untouched); needs a page of the site open'),
536
+ 'to the site locale\'s (edited text untouched), and turn a gallery line thumbnail into a ' +
537
+ 'single image; needs a page of the site open'),
537
538
  dry_run: z.boolean().optional(),
538
539
  },
539
540
  annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: true },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sbuilder-mcp",
3
- "version": "0.69.0",
3
+ "version": "0.71.0",
4
4
  "description": "MCP server that designs and operates a Store Builder site — pages, data, theme and publish — through the platform's own API and live-edit protocol.",
5
5
  "mcpName": "io.github.vuluu2k/sbuilder-mcp",
6
6
  "type": "module",