sbuilder-mcp 0.66.1 → 0.67.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,18 @@ 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.67.0] - 2026-09-25
10
+
11
+ ### Added
12
+ - `sb_review` warns once per site (`cartDrawerLanguage`) when the site's cart drawer still carries another locale's seed words, and `sb_store` action `cart` gains a `relocalize` argument that rewrites exactly those words to the site locale's seed, leaving merchant-edited text untouched; a real run needs a page of the site open, since the drawer is written through the page save.
13
+
14
+ ### Changed
15
+ - The generated catalog was regenerated against the platform: a category/search card's media-dataset repeater now shows a single thumbnail instead of a strip, and several defaults (form/menu hover, cart-count badge) now read the theme's `var(--wb-sc-*)` tokens instead of a hardcoded color.
16
+
17
+ ### Fixed
18
+ - A non-GET write to a shared global section, or a global/overlay frame from the live room, now marks the open page's copy stale when it composes that master, so the next save re-pulls the page instead of decomposing an old header, footer, or cart drawer back over the merchant's change.
19
+ - `sb_store` action `cart`'s dry run now explains why a newly created drawer would come out in English — unreadable site settings, an unset `settings.locale`, or a locale with no seed — instead of staying silent about the fallback.
20
+
9
21
  ## [0.66.1] - 2026-09-25
10
22
 
11
23
  ### Fixed
package/CHANGELOG.vi.md CHANGED
@@ -6,6 +6,18 @@ 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.67.0] - 2026-09-25
10
+
11
+ ### Added
12
+ - `sb_review` cảnh báo một lần cho mỗi site (`cartDrawerLanguage`) khi cart drawer của site vẫn còn mang từ seed của một locale khác, và `sb_store` action `cart` có thêm tham số `relocalize` để viết lại đúng những từ đó thành seed của locale site, giữ nguyên text đã được merchant chỉnh sửa; một lần chạy thật cần có một trang của site đang mở, vì drawer được ghi thông qua lần lưu trang.
13
+
14
+ ### Changed
15
+ - Catalog được sinh ra đã được làm mới dựa trên nền tảng: repeater media-dataset trên thẻ category/search giờ hiện một thumbnail duy nhất thay vì một dải ảnh, và một số giá trị mặc định (hover của form/menu, badge cart-count) giờ đọc theo token `var(--wb-sc-*)` của theme thay vì màu cố định.
16
+
17
+ ### Fixed
18
+ - Một lần ghi không phải GET vào một global section dùng chung, hoặc một frame global/overlay từ phòng live, giờ đánh dấu bản sao của trang đang mở là cũ khi nó compose từ master đó, để lần lưu tiếp theo đọc lại trang thay vì decompose header, footer hay cart drawer cũ đè lên thay đổi của merchant.
19
+ - Dry run của action `cart` trong `sb_store` giờ giải thích lý do vì sao một drawer mới tạo sẽ ra tiếng Anh — không đọc được settings của site, `settings.locale` chưa được đặt, hoặc một locale không có seed — thay vì im lặng về việc rơi về tiếng Anh.
20
+
9
21
  ## [0.66.1] - 2026-09-25
10
22
 
11
23
  ### Fixed
package/README.md CHANGED
@@ -111,7 +111,7 @@ in this client: one would put the very key the platform exists to hold back into
111
111
  | `sb_import` | Read a page from any public URL and add its structure and content to the open page as real elements, styled with THIS page's own tokens — a translation, not a clone |
112
112
  | `sb_import_site` | Read a WHOLE site from one URL — its sitemap, or the links on that page — and give each page found its own draft page here, built from this site's tokens; the entry page's own colours and type scale also patch into this SITE'S theme, so it stops being purely a read |
113
113
  | `sb_theme` | Read or patch the site's palette and type scale — the layer every style preset resolves from, so one token repaints every page; `locale` sets the site's language |
114
- | `sb_store` | Run a store flow that must happen in a fixed order — `checkout` (the four writes that make a working one), `form` (any of the platform's 17 templates with its own field document), `chrome` (one shared header or footer on a real site menu — desktop menu, mobile drawer, cart and account icons), `menu` (a menu node bound to the site's menu, its links resolved), `overlay_attach` (a pop-up or quick view on the open page), `cart` (the site's cart drawer, when it has none) and `app` (a built-in app plus the pages it needs), `global_attach` / `global_detach` (put an EXISTING shared section on the open page, or take it off) |
114
+ | `sb_store` | Run a store flow that must happen in a fixed order — `checkout` (the four writes that make a working one), `form` (any of the platform's 17 templates with its own field document), `chrome` (one shared header or footer on a real site menu — desktop menu, mobile drawer, cart and account icons), `menu` (a menu node bound to the site's menu, its links resolved), `overlay_attach` (a pop-up or quick view on the open page), `cart` (the site's cart drawer, when it has none; `relocalize` puts an existing one in the site's language) and `app` (a built-in app plus the pages it needs), `global_attach` / `global_detach` (put an EXISTING shared section on the open page, or take it off) |
115
115
  | `sb_undo` | Put back what a PUT replaced. The SECOND answer for a page, not the only one: the platform has versions, history and restore (`sb_api_find` "page versions"), which outlive this process — reach for those first and use this for every other shaped PUT |
116
116
 
117
117
  Twenty-eight tools, **568 API operations** (195 of the 253 writes carrying a body shape read
package/README.vi.md CHANGED
@@ -108,7 +108,7 @@ nhét ngược lại vào mọi bản cài.
108
108
  | `sb_import` | Đọc một trang từ URL công khai bất kỳ và thêm cấu trúc + nội dung của nó vào trang đang mở dưới dạng element thật, mang token của CHÍNH trang này — là dịch lại, không phải sao chép |
109
109
  | `sb_import_site` | Đọc CẢ website từ một URL — sitemap của nó, hoặc các link trên trang đó — và tạo cho mỗi trang tìm được một trang nháp riêng ở đây, dựng bằng token của site này; màu và thang chữ của trang gốc cũng được vá vào THEME của site này, nên đây không còn thuần là đọc |
110
110
  | `sb_theme` | Đọc hoặc vá bảng màu và thang chữ của site — tầng mà mọi style preset phân giải từ đó, nên một token thay áo cho mọi trang; `locale` đặt ngôn ngữ của site |
111
- | `sb_store` | Chạy một luồng cửa hàng bắt buộc đúng thứ tự — `checkout` (bốn lệnh ghi tạo nên trang thanh toán), `form` (một trong 17 template của nền tảng kèm field document của nó), `chrome` (một header hoặc footer dùng chung trên menu thật của site — menu desktop, drawer mobile, icon giỏ hàng và tài khoản), `menu` (một node menu bind vào menu của site, link đã phân giải), `overlay_attach` (một pop-up hay quick view trên trang đang mở), `cart` (ngăn giỏ hàng của site, khi chưa có), `app` (một app dựng sẵn kèm những trang nó cần), và `global_attach` / `global_detach` (đặt một section dùng chung ĐÃ CÓ lên trang đang mở, hoặc gỡ ra) |
111
+ | `sb_store` | Chạy một luồng cửa hàng bắt buộc đúng thứ tự — `checkout` (bốn lệnh ghi tạo nên trang thanh toán), `form` (một trong 17 template của nền tảng kèm field document của nó), `chrome` (một header hoặc footer dùng chung trên menu thật của site — menu desktop, drawer mobile, icon giỏ hàng và tài khoản), `menu` (một node menu bind vào menu của site, link đã phân giải), `overlay_attach` (một pop-up hay quick view trên trang đang mở), `cart` (ngăn giỏ hàng của site, khi chưa có; `relocalize` đưa ngăn giỏ đã có về ngôn ngữ của site), `app` (một app dựng sẵn kèm những trang nó cần), và `global_attach` / `global_detach` (đặt một section dùng chung ĐÃ CÓ lên trang đang mở, hoặc gỡ ra) |
112
112
  | `sb_undo` | Trả lại thứ mà một lệnh PUT đã ghi đè. Với TRANG thì đây là đường về thứ hai chứ không phải duy nhất: nền tảng có version, history và restore (`sb_api_find` "page versions"), và chúng sống lâu hơn tiến trình này — hãy dùng chúng trước, còn tool này cho mọi PUT có hình dạng khác |
113
113
 
114
114
  Hai mươi tám tool, **568 operation API** (195 trong 253 lệnh ghi có hình dạng body đọc thẳng
@@ -70,7 +70,7 @@ export const CHECKOUT_FORM_DOCUMENT = {
70
70
  "lineHeight": "130%",
71
71
  "fontSize": "20px",
72
72
  "fontWeight": "600",
73
- "color": "#171717"
73
+ "color": "var(--wb-sc-heading, #171717)"
74
74
  },
75
75
  "config": {},
76
76
  "specials": {
@@ -902,7 +902,7 @@ export const FORM_TEMPLATES = {
902
902
  "lineHeight": "130%",
903
903
  "fontSize": "20px",
904
904
  "fontWeight": "600",
905
- "color": "#171717"
905
+ "color": "var(--wb-sc-heading, #171717)"
906
906
  },
907
907
  "config": {},
908
908
  "specials": {
@@ -1168,7 +1168,7 @@ export const FORM_TEMPLATES = {
1168
1168
  "lineHeight": "130%",
1169
1169
  "fontSize": "20px",
1170
1170
  "fontWeight": "600",
1171
- "color": "#171717"
1171
+ "color": "var(--wb-sc-heading, #171717)"
1172
1172
  },
1173
1173
  "config": {},
1174
1174
  "specials": {
@@ -1345,7 +1345,7 @@ export const FORM_TEMPLATES = {
1345
1345
  "lineHeight": "130%",
1346
1346
  "fontSize": "20px",
1347
1347
  "fontWeight": "600",
1348
- "color": "#171717"
1348
+ "color": "var(--wb-sc-heading, #171717)"
1349
1349
  },
1350
1350
  "config": {},
1351
1351
  "specials": {
@@ -1745,7 +1745,7 @@ export const FORM_TEMPLATES = {
1745
1745
  "lineHeight": "130%",
1746
1746
  "fontSize": "20px",
1747
1747
  "fontWeight": "600",
1748
- "color": "#171717"
1748
+ "color": "var(--wb-sc-heading, #171717)"
1749
1749
  },
1750
1750
  "config": {},
1751
1751
  "specials": {
@@ -2255,7 +2255,7 @@ export const FORM_TEMPLATES = {
2255
2255
  "lineHeight": "130%",
2256
2256
  "fontSize": "20px",
2257
2257
  "fontWeight": "600",
2258
- "color": "#171717"
2258
+ "color": "var(--wb-sc-heading, #171717)"
2259
2259
  },
2260
2260
  "config": {},
2261
2261
  "specials": {
@@ -2627,7 +2627,7 @@ export const FORM_TEMPLATES = {
2627
2627
  "lineHeight": "130%",
2628
2628
  "fontSize": "20px",
2629
2629
  "fontWeight": "600",
2630
- "color": "#171717"
2630
+ "color": "var(--wb-sc-heading, #171717)"
2631
2631
  },
2632
2632
  "config": {},
2633
2633
  "specials": {
@@ -2993,7 +2993,7 @@ export const FORM_TEMPLATES = {
2993
2993
  "lineHeight": "130%",
2994
2994
  "fontSize": "20px",
2995
2995
  "fontWeight": "600",
2996
- "color": "#171717"
2996
+ "color": "var(--wb-sc-heading, #171717)"
2997
2997
  },
2998
2998
  "config": {},
2999
2999
  "specials": {
@@ -3248,7 +3248,7 @@ export const FORM_TEMPLATES = {
3248
3248
  "lineHeight": "130%",
3249
3249
  "fontSize": "20px",
3250
3250
  "fontWeight": "600",
3251
- "color": "#171717"
3251
+ "color": "var(--wb-sc-heading, #171717)"
3252
3252
  },
3253
3253
  "config": {},
3254
3254
  "specials": {
@@ -3547,7 +3547,7 @@ export const FORM_TEMPLATES = {
3547
3547
  "lineHeight": "130%",
3548
3548
  "fontSize": "20px",
3549
3549
  "fontWeight": "600",
3550
- "color": "#171717"
3550
+ "color": "var(--wb-sc-heading, #171717)"
3551
3551
  },
3552
3552
  "config": {},
3553
3553
  "specials": {
@@ -3914,7 +3914,7 @@ export const FORM_TEMPLATES = {
3914
3914
  "lineHeight": "130%",
3915
3915
  "fontSize": "20px",
3916
3916
  "fontWeight": "600",
3917
- "color": "#171717"
3917
+ "color": "var(--wb-sc-heading, #171717)"
3918
3918
  },
3919
3919
  "config": {},
3920
3920
  "specials": {
@@ -4138,7 +4138,7 @@ export const FORM_TEMPLATES = {
4138
4138
  "lineHeight": "130%",
4139
4139
  "fontSize": "20px",
4140
4140
  "fontWeight": "600",
4141
- "color": "#171717"
4141
+ "color": "var(--wb-sc-heading, #171717)"
4142
4142
  },
4143
4143
  "config": {},
4144
4144
  "specials": {
@@ -4401,7 +4401,7 @@ export const FORM_TEMPLATES = {
4401
4401
  "lineHeight": "130%",
4402
4402
  "fontSize": "20px",
4403
4403
  "fontWeight": "600",
4404
- "color": "#171717"
4404
+ "color": "var(--wb-sc-heading, #171717)"
4405
4405
  },
4406
4406
  "config": {},
4407
4407
  "specials": {
@@ -4670,7 +4670,7 @@ export const FORM_TEMPLATES = {
4670
4670
  "lineHeight": "130%",
4671
4671
  "fontSize": "20px",
4672
4672
  "fontWeight": "600",
4673
- "color": "#171717"
4673
+ "color": "var(--wb-sc-heading, #171717)"
4674
4674
  },
4675
4675
  "config": {},
4676
4676
  "specials": {
@@ -4969,7 +4969,7 @@ export const FORM_TEMPLATES = {
4969
4969
  "lineHeight": "130%",
4970
4970
  "fontSize": "20px",
4971
4971
  "fontWeight": "600",
4972
- "color": "#171717"
4972
+ "color": "var(--wb-sc-heading, #171717)"
4973
4973
  },
4974
4974
  "config": {},
4975
4975
  "specials": {
@@ -5216,7 +5216,7 @@ export const FORM_TEMPLATES = {
5216
5216
  "lineHeight": "130%",
5217
5217
  "fontSize": "20px",
5218
5218
  "fontWeight": "600",
5219
- "color": "#171717"
5219
+ "color": "var(--wb-sc-heading, #171717)"
5220
5220
  },
5221
5221
  "config": {},
5222
5222
  "specials": {
@@ -5511,7 +5511,7 @@ export const FORM_TEMPLATES = {
5511
5511
  "lineHeight": "130%",
5512
5512
  "fontSize": "20px",
5513
5513
  "fontWeight": "600",
5514
- "color": "#171717"
5514
+ "color": "var(--wb-sc-heading, #171717)"
5515
5515
  },
5516
5516
  "config": {},
5517
5517
  "specials": {
@@ -5686,7 +5686,7 @@ export const FORM_TEMPLATES = {
5686
5686
  "lineHeight": "130%",
5687
5687
  "fontSize": "20px",
5688
5688
  "fontWeight": "600",
5689
- "color": "#171717"
5689
+ "color": "var(--wb-sc-heading, #171717)"
5690
5690
  },
5691
5691
  "config": {},
5692
5692
  "specials": {
@@ -5874,7 +5874,7 @@ export const FORM_TEMPLATES = {
5874
5874
  "lineHeight": "130%",
5875
5875
  "fontSize": "20px",
5876
5876
  "fontWeight": "600",
5877
- "color": "#171717"
5877
+ "color": "var(--wb-sc-heading, #171717)"
5878
5878
  },
5879
5879
  "config": {},
5880
5880
  "specials": {
@@ -1,6 +1,6 @@
1
1
  export const DEAD_KEY_SOURCE = {
2
2
  "files": 4426,
3
- "identifiers": 83749,
3
+ "identifiers": 83752,
4
4
  "seededKeys": 436,
5
5
  "dead": 0
6
6
  };
@@ -9376,7 +9376,7 @@ export const ELEMENTS = {
9376
9376
  "lineHeight": "130%",
9377
9377
  "fontSize": "20px",
9378
9378
  "fontWeight": "600",
9379
- "color": "#171717"
9379
+ "color": "var(--wb-sc-heading, #171717)"
9380
9380
  }
9381
9381
  },
9382
9382
  "inspector": [
@@ -13819,7 +13819,7 @@ export const ELEMENTS = {
13819
13819
  "states": {
13820
13820
  "hover": {
13821
13821
  "style": {
13822
- "color": "#676767"
13822
+ "color": "var(--wb-sc-text, #676767)"
13823
13823
  }
13824
13824
  }
13825
13825
  }
@@ -13942,7 +13942,7 @@ export const ELEMENTS = {
13942
13942
  "contentTips": [
13943
13943
  "this skin carries no text of its own — labels come from the menu data",
13944
13944
  "Default and Hover are separate state slots; edit Hover with the State switcher",
13945
- "the default look is the theme link colour (ink #171717 on a default theme) and the text-2 font, with a #676767 hover"
13945
+ "the default look is the theme link colour (ink #171717 on a default theme) and the text-2 font, with a hover in the theme body-text colour"
13946
13946
  ],
13947
13947
  "semantics": [
13948
13948
  "menu-item",
@@ -14834,8 +14834,8 @@ export const ELEMENTS = {
14834
14834
  "paddingLeft": "6px",
14835
14835
  "paddingRight": "6px",
14836
14836
  "borderRadius": "999px",
14837
- "backgroundColor": "#171717",
14838
- "color": "#ffffff",
14837
+ "backgroundColor": "var(--wb-sc-buttonBg, #171717)",
14838
+ "color": "var(--wb-sc-buttonText, #ffffff)",
14839
14839
  "fontSize": "12px",
14840
14840
  "fontWeight": "600",
14841
14841
  "lineHeight": "1"
@@ -26460,8 +26460,8 @@ export const ELEMENTS = {
26460
26460
  "height": "fit-content",
26461
26461
  "padding": "2px 8px",
26462
26462
  "borderRadius": "4px",
26463
- "backgroundColor": "#171717",
26464
- "color": "#ffffff",
26463
+ "backgroundColor": "var(--wb-sc-buttonBg, #171717)",
26464
+ "color": "var(--wb-sc-buttonText, #ffffff)",
26465
26465
  "fontSize": "12px",
26466
26466
  "fontWeight": "600",
26467
26467
  "lineHeight": "1"
@@ -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": "eb36ffb27d036e14093aa524c3179f4d1d193d77",
5
- "committedAt": "2026-09-25T21:24:32+07:00",
4
+ "commit": "2fba72c10b84e624cb05709f9759bba93d36dbec",
5
+ "committedAt": "2026-09-25T22:15:50+07:00",
6
6
  "dirty": false
7
7
  };
@@ -3100,7 +3100,7 @@ export const STORE_PAGE_SEEDS = {
3100
3100
  "alt": ""
3101
3101
  }
3102
3102
  ],
3103
- "layout": "bottom",
3103
+ "layout": "single",
3104
3104
  "activeIndex": 0
3105
3105
  },
3106
3106
  "specials": {},
@@ -3812,7 +3812,7 @@ export const STORE_PAGE_SEEDS = {
3812
3812
  "alt": ""
3813
3813
  }
3814
3814
  ],
3815
- "layout": "bottom",
3815
+ "layout": "single",
3816
3816
  "activeIndex": 0
3817
3817
  },
3818
3818
  "specials": {},
@@ -0,0 +1,58 @@
1
+ import { walk } from '../../core/tree.js';
2
+ import { CART_SEEDS } from '../../catalog/overlays.generated.js';
3
+ /**
4
+ * A CART DRAWER IN ANOTHER LANGUAGE THAN ITS SITE. A store made before the
5
+ * per-locale seed keeps "Your cart" / "Checkout" / "Your cart is empty" on a
6
+ * Vietnamese site, and nothing on the page says so. Only strings EXACTLY equal
7
+ * to another locale's seed word count — anything else is the merchant's text.
8
+ */
9
+ const KEYS = ['text', 'label', 'emptyText'];
10
+ /** The seed a site's `settings.locale` picks: primary subtag, `en` when there is none. */
11
+ export function cartSeedLocale(locale) {
12
+ const l = String(locale ?? '').toLowerCase().split('-')[0];
13
+ return Object.hasOwn(CART_SEEDS, l) ? l : 'en';
14
+ }
15
+ const order = (d) => {
16
+ const out = [];
17
+ walk(d, d.root_node_id, (n) => out.push(n));
18
+ return out;
19
+ };
20
+ /**
21
+ * `key \0 other-locale word` → the site-locale word, by pairing each other
22
+ * seed's nodes with the target seed's in walk order (same seed shape, so the
23
+ * same position is the same word). A pair whose types differ is skipped.
24
+ */
25
+ function seedWords(to) {
26
+ const target = order(CART_SEEDS[to]);
27
+ const out = new Map();
28
+ for (const [l, seed] of Object.entries(CART_SEEDS)) {
29
+ if (l === to)
30
+ continue;
31
+ order(seed).forEach((n, i) => {
32
+ const t = target[i];
33
+ if (!t || t.data.type !== n.data.type)
34
+ return;
35
+ for (const k of KEYS) {
36
+ const a = n.specials?.[k];
37
+ const b = t.specials?.[k];
38
+ if (typeof a === 'string' && typeof b === 'string' && a !== b)
39
+ out.set(`${k}\0${a}`, b);
40
+ }
41
+ });
42
+ }
43
+ return out;
44
+ }
45
+ /** Every seed word under `rootId` that belongs to a locale other than `locale`'s seed. */
46
+ export function cartRelocalize(doc, rootId, locale) {
47
+ const words = seedWords(locale);
48
+ const out = [];
49
+ walk(doc, rootId, (n) => {
50
+ for (const key of KEYS) {
51
+ const from = n.specials?.[key];
52
+ const to = typeof from === 'string' ? words.get(`${key}\0${from}`) : undefined;
53
+ if (to !== undefined)
54
+ out.push({ node_id: n.id, key, from: from, to });
55
+ }
56
+ });
57
+ return out;
58
+ }
@@ -62,6 +62,8 @@ export async function gatherReadiness(ctx, siteId, pageNodes, openPageId) {
62
62
  get(`/api/sites/${site}`),
63
63
  // THE OVERLAYS, for the one a cart control opens: no `cart` overlay means
64
64
  // every `open_cart` on the site opens nothing.
65
+ // The list ships each overlay's FULL document (overlays/rest/rest.go), so the
66
+ // cart drawer's words are read here too — see cartDrawerLanguage.
65
67
  get(`/api/sites/${site}/overlays`),
66
68
  // `<html lang>` is served from this; see siteLanguage in readiness.ts.
67
69
  get(`/api/sites/${site}/settings`),
@@ -116,6 +118,9 @@ export async function gatherReadiness(ctx, siteId, pageNodes, openPageId) {
116
118
  pageNodes,
117
119
  globalNodes,
118
120
  globalKinds,
121
+ cartOverlay: Array.isArray(overlayList?.overlays)
122
+ ? (overlayList.overlays.find((o) => o.kind === 'cart')?.document ?? null)
123
+ : null,
119
124
  overlayKinds: Array.isArray(overlayList?.overlays) ? overlayList.overlays.map((o) => o.kind ?? '') : null,
120
125
  categories,
121
126
  categoryPageLinks,
@@ -16,6 +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
20
  /** The purchase binding an Add-to-cart / Buy-now button carries. */
20
21
  const PRODUCT_ACTION_BINDING_ID = 'bind-product-action';
21
22
  /** Element types that only make sense in a store. */
@@ -262,6 +263,23 @@ export function readinessGaps(input) {
262
263
  : `Create a page of type "${type}" and publish it.`,
263
264
  });
264
265
  }
266
+ // A CART DRAWER IN ANOTHER LANGUAGE. Asked before the store gate: a drawer
267
+ // that exists is shown on every page, whatever this page sells.
268
+ if (input.siteLocale && input.cartOverlay?.root_node_id) {
269
+ const locale = cartSeedLocale(input.siteLocale);
270
+ const changes = cartRelocalize(input.cartOverlay, input.cartOverlay.root_node_id, locale);
271
+ if (changes.length > 0) {
272
+ gaps.push({
273
+ id: 'cartDrawerLanguage',
274
+ draft: false,
275
+ problem: `The site's locale is "${input.siteLocale}", but the cart drawer still carries another ` +
276
+ `language's seed words: ${[...new Set(changes.map((c) => `"${c.from}"`))].join(', ')}. ` +
277
+ 'Every page shows it.',
278
+ fix: 'Open any page of this site, then sb_store action:"cart" relocalize:true dry_run:false — it ' +
279
+ `rewrites exactly those seed strings to the "${locale}" seed's, and leaves edited text alone.`,
280
+ });
281
+ }
282
+ }
265
283
  if (!isStore(input))
266
284
  return gaps;
267
285
  if (pages && !published(pages, 'checkout')) {
@@ -231,6 +231,13 @@ export class LiveSession {
231
231
  }
232
232
  break;
233
233
  }
234
+ case 'global':
235
+ case 'overlay': {
236
+ const id = e.t === 'global' ? e.globalId : e.overlayId;
237
+ if (typeof id === 'string' && id)
238
+ this.opts.onMaster?.(e.t, id, String(e.op ?? ''), Number(e.rev ?? 0));
239
+ break;
240
+ }
234
241
  case 'snapreq':
235
242
  // THE YIELD RULE. Never answered. Sending a snapshot would make this
236
243
  // client an authority on the document — exactly the position it declines
@@ -260,6 +260,7 @@ export function joinRoom(ctx, session, siteId) {
260
260
  onRemote: (patches) => session.applyRemote(patches),
261
261
  onDesync: (reason) => session.markStale(reason),
262
262
  onSource: (pageId, rev, peerId) => session.sourceSaved(pageId, rev, peerId),
263
+ onMaster: (kind, id, op, rev) => session.masterSaved(kind, id, op, rev),
263
264
  });
264
265
  socket.connect();
265
266
  session.attachLive(live, siteId);
@@ -57,6 +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
61
  const KIND_NAME = {
61
62
  popup: 'Pop-up',
62
63
  quickview: 'Quick view',
@@ -306,12 +307,18 @@ export async function ensureCartDrawer(ctx, session, siteId, dryRun) {
306
307
  }).catch(() => null));
307
308
  const locale = String(settings?.settings?.locale ?? '').toLowerCase().split('-')[0];
308
309
  const seedLocale = Object.hasOwn(CART_SEEDS, locale) ? locale : 'en';
310
+ const languageNote = seedLocale === locale
311
+ ? undefined
312
+ : settings === null
313
+ ? 'The site settings could not be read, so the drawer is in English — edit its words if the store is not.'
314
+ : `settings.locale is ${locale ? `"${locale}", which has no seed` : 'unset'}, so the drawer is in English — set it with sb_theme locale first if the store is not.`;
309
315
  const seed = CART_SEEDS[seedLocale] ?? OVERLAY_SEEDS.cart;
310
316
  const body = { kind: 'cart', name: 'Cart', document: withFreshIds(seed) };
311
317
  if (dryRun) {
312
318
  return {
313
319
  dry_run: true,
314
320
  language: seedLocale,
321
+ ...(languageNote ? { language_note: languageNote } : {}),
315
322
  plan: redact([{ step: 1, what: 'create the site\'s cart drawer', method: 'POST', path, body }]),
316
323
  note: 'Nothing was sent. Re-call with dry_run:false to create it; it shows on every page.',
317
324
  };
@@ -332,7 +339,65 @@ export async function ensureCartDrawer(ctx, session, siteId, dryRun) {
332
339
  overlay_id: made.overlay.id,
333
340
  created: true,
334
341
  language: seedLocale,
342
+ ...(languageNote ? { language_note: languageNote } : {}),
335
343
  ...(nodeId ? { node_id: nodeId } : {}),
336
344
  next: 'Publish the pages: the drawer reaches a shopper through each published page.',
337
345
  };
338
346
  }
347
+ /**
348
+ * `sb_store action:"cart" relocalize:true` — PUT THE SITE'S LANGUAGE ON AN
349
+ * EXISTING DRAWER. Rewrites only strings EXACTLY equal to another locale's seed
350
+ * word, node ids and styles untouched.
351
+ *
352
+ * THROUGH THE PAGE SAVE, never the overlays API: `PATCH /overlays/{id}` ignores
353
+ * a `document` and answers 200, and there is no document PUT — the page save
354
+ * carries the composed drawer and the page context decomposes it into the
355
+ * master (overlays/rest/rest.go). So a real run needs a page of this site open.
356
+ */
357
+ export async function relocalizeCartDrawer(ctx, session, siteId, dryRun) {
358
+ const site = encodeURIComponent(siteId);
359
+ const get = (path) => request({ base: ctx.base, method: 'GET', path, token: siteToken(ctx), fetchImpl: ctx.fetchImpl });
360
+ const listed = (await get(`/api/sites/${site}/overlays`));
361
+ const cart = (listed.overlays ?? []).find((o) => o.kind === 'cart');
362
+ if (!cart?.id)
363
+ throw new Error('sbuilder: this site has no cart drawer — sb_store action:"cart" without relocalize creates one.');
364
+ const settings = (await get(`/api/sites/${site}/settings`));
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);
370
+ const open = session.peek() ? session.location() : null;
371
+ const composed = open?.siteId === siteId
372
+ ? Object.values(session.current().doc.nodes).find((n) => n.specials?.overlayId === cart.id)
373
+ : undefined;
374
+ const changes = composed
375
+ ? cartRelocalize(session.current().doc, composed.id, language)
376
+ : cart.document?.root_node_id
377
+ ? cartRelocalize(cart.document, cart.document.root_node_id, language)
378
+ : [];
379
+ if (dryRun || changes.length === 0) {
380
+ return {
381
+ ...(dryRun ? { dry_run: true } : {}),
382
+ overlay_id: cart.id,
383
+ language,
384
+ changes,
385
+ ...(changes.length === 0
386
+ ? { note: 'Nothing to change — no seed word from another language is left in the drawer.' }
387
+ : {
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` }]),
389
+ note: 'Nothing was sent. Re-call with dry_run:false; it writes through the open page\'s save.',
390
+ }),
391
+ };
392
+ }
393
+ if (!composed) {
394
+ 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
+ }
396
+ await session.applyAndSave(changes.map((c) => ({ op: 'set', path: ['nodes', c.node_id, 'specials', c.key], value: c.to })));
397
+ return {
398
+ overlay_id: cart.id,
399
+ language,
400
+ changes,
401
+ next: 'Publish the pages: the drawer reaches a shopper through each published page.',
402
+ };
403
+ }
@@ -17,7 +17,8 @@ import { animationNote, deadKeyNote, unknownValueNote, unknownWriteNote, precond
17
17
  import { skinLevelNote } from '../domains/site/fieldskin.js';
18
18
  import { siteTheme } from '../domains/site/theme-fetch.js';
19
19
  import { ensureSiteTheme } from './theme.js';
20
- import { request, redact, watchPageWrites, onPageSourceWrite } from '../transport/http.js';
20
+ import { request, redact, watchPageWrites, onPageSourceWrite, onGlobalWrite } from '../transport/http.js';
21
+ import { SPEC_GLOBAL_ID, SPEC_GLOBAL_REV, SPEC_OVERLAY_ID, SPEC_OVERLAY_REV } from '../core/tree.js';
21
22
  import { siteToken } from './credentialpick.js';
22
23
  import { validateForSave } from '../domains/site/validate.js';
23
24
  import { reviewDesign, REVIEW_NOTICE } from '../domains/site/review.js';
@@ -112,6 +113,41 @@ export class PageSession {
112
113
  if (doc !== this.doc?.doc && this.isOpen(site, page))
113
114
  this.markStale('the page was written by another call');
114
115
  });
116
+ onGlobalWrite((site, id) => {
117
+ if (this.siteId === site && this.composedMaster('global', id)) {
118
+ this.markStale(`shared section ${id} was written by another call`);
119
+ }
120
+ });
121
+ }
122
+ /** The node this copy composed from that master (stamped `globalId` / `overlayId`), if any. */
123
+ composedMaster(kind, id) {
124
+ if (!this.doc)
125
+ return undefined;
126
+ const stamp = kind === 'global' ? SPEC_GLOBAL_ID : SPEC_OVERLAY_ID;
127
+ return Object.values(this.doc.doc.nodes).find((n) => n.specials?.[stamp] === id);
128
+ }
129
+ /**
130
+ * A shared master was saved or deleted (the room's `global` / `overlay`
131
+ * frame). Past the rev this copy composed — or re-stamped from its OWN save,
132
+ * whose echo this is otherwise — the copy is stale: the yield rule, the next
133
+ * write re-pulls first. `meta` is a rename, and moves no content. Judged
134
+ * after any save in flight, whose re-stamp carries the new rev.
135
+ */
136
+ masterSaved(kind, id, op, rev) {
137
+ if (op === 'meta')
138
+ return;
139
+ const check = () => {
140
+ const n = this.composedMaster(kind, id);
141
+ if (!n || this.siteId !== this.liveSite)
142
+ return;
143
+ const had = Number(n.specials?.[kind === 'global' ? SPEC_GLOBAL_REV : SPEC_OVERLAY_REV] ?? 0);
144
+ if (op === 'deleted' || rev > had)
145
+ this.markStale(`${kind} ${id} was ${op} elsewhere (rev ${rev})`);
146
+ };
147
+ if (this.saving)
148
+ void this.saving.then(check, check);
149
+ else
150
+ check();
115
151
  }
116
152
  /**
117
153
  * ONE ROOM AT A TIME. A second join (sb_live_join again, or a page on another
@@ -44,7 +44,7 @@ import { siteChrome, wearChrome } from './page.js';
44
44
  import { addSubtree } from '../domains/site/builder.js';
45
45
  import { attachGlobal, buildChrome, detachGlobal } from './chrome.js';
46
46
  import { bindMenu } from './menu.js';
47
- import { attachOverlay, ensureCartDrawer } from './overlay.js';
47
+ import { attachOverlay, ensureCartDrawer, relocalizeCartDrawer } from './overlay.js';
48
48
  import { installApp } from './app.js';
49
49
  import { BUILTIN_APP_KEYS } from '../catalog/appscaffolds.generated.js';
50
50
  import { ELEMENTS } from '../catalog/elements.generated.js';
@@ -436,10 +436,15 @@ export function registerStoreTools(server, ctx, session) {
436
436
  .optional()
437
437
  .describe('action:"global_attach"/"global_detach" — the shared section; omitted on attach it ' +
438
438
  'lists the site\'s own'),
439
+ relocalize: z
440
+ .boolean()
441
+ .optional()
442
+ .describe('action:"cart" — rewrite an EXISTING drawer\'s seed words that are in another language ' +
443
+ 'to the site locale\'s (edited text untouched); needs a page of the site open'),
439
444
  dry_run: z.boolean().optional(),
440
445
  },
441
446
  annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: true },
442
- }, async ({ action, site_id: given, language, page_name, headline, template, name, footer, node_id, menu_id, kind, overlay_id, list_id, app_key, global_id, dry_run, }) => {
447
+ }, async ({ action, site_id: given, language, page_name, headline, template, name, footer, node_id, menu_id, kind, overlay_id, list_id, app_key, global_id, relocalize, dry_run, }) => {
443
448
  const siteId = siteFor(ctx, given);
444
449
  if (action === 'global_attach' || action === 'global_detach') {
445
450
  // THE COMPOSED STAMP IS NEVER THE CALLER'S TO WRITE. A page references
@@ -498,7 +503,8 @@ export function registerStoreTools(server, ctx, session) {
498
503
  }));
499
504
  }
500
505
  if (action === 'cart') {
501
- return text(await ensureCartDrawer(ctx, session, siteId, dry_run !== false));
506
+ const run = relocalize === true ? relocalizeCartDrawer : ensureCartDrawer;
507
+ return text(await run(ctx, session, siteId, dry_run !== false));
502
508
  }
503
509
  if (action === 'chrome') {
504
510
  return text(await buildChrome(ctx, session, siteId, footer === true ? 'footer' : 'header', {
@@ -134,6 +134,14 @@ export function onPageSourceWrite(fn) {
134
134
  sourceWritten.delete(fn);
135
135
  };
136
136
  }
137
+ const globalWritten = new Set();
138
+ export function onGlobalWrite(fn) {
139
+ globalWritten.add(fn);
140
+ return () => {
141
+ globalWritten.delete(fn);
142
+ };
143
+ }
144
+ const GLOBAL_WRITE = /^\/api\/sites\/([^/]+)\/global-sections\/([^/]+)(?:\/document)?$/;
137
145
  const SOURCE_WRITE = /^\/api\/sites\/([^/]+)\/pages\/([^/]+)\/source$/;
138
146
  /**
139
147
  * A restore replaces the draft ON THE PLATFORM with no document in the body —
@@ -151,6 +159,13 @@ const SOURCE_RESTORE = /^\/api\/sites\/([^/]+)\/pages\/([^/]+)\/(?:versions|hist
151
159
  * peer is on that page) and published as node-level ops.
152
160
  */
153
161
  export async function request(opts) {
162
+ const global = opts.method !== 'GET' ? GLOBAL_WRITE.exec(opts.path) : null;
163
+ if (global) {
164
+ const out = await send(opts);
165
+ for (const fn of globalWritten)
166
+ fn(decodeURIComponent(global[1]), decodeURIComponent(global[2]));
167
+ return out;
168
+ }
154
169
  const restore = opts.method === 'POST' ? SOURCE_RESTORE.exec(opts.path) : null;
155
170
  if (restore) {
156
171
  const out = await send(opts);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sbuilder-mcp",
3
- "version": "0.66.1",
3
+ "version": "0.67.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",