sbuilder-mcp 0.4.0 → 0.4.2

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,17 @@ 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.4.2] - 2026-09-08
10
+
11
+ ### Changed
12
+ - sb_look now waits for the page to stop changing (a MutationObserver, 250 ms quiet / 2000 ms cap) instead of waiting on a networkidle timeout that a storefront's polling cart island and session checks could never satisfy, cutting a typical shot from ~3.2s to under 1s while still photographing a page that never settles rather than holding the shot forever.
13
+
14
+ ## [0.4.1] - 2026-09-08
15
+
16
+ ### Fixed
17
+ - sb_review now reports findings inside the cart drawer and other site overlays, tagged with overlay: true, instead of silently skipping them on the belief that an overlay is not the page's to fix; the review notice explains that an overlay:true finding is fixed the same way but is site-wide, so it only needs fixing once.
18
+ - sb_api_call's path_params now matches a parameter name case-insensitively as a fallback, so a call using the common siteId spelling no longer fails against the 8 operations that spell it siteID; an exact match still wins and a genuinely missing parameter is still refused.
19
+
9
20
  ## [0.4.0] - 2026-09-08
10
21
 
11
22
  ### Added
package/CHANGELOG.vi.md CHANGED
@@ -6,6 +6,17 @@ 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.4.2] - 2026-09-08
10
+
11
+ ### Changed
12
+ - sb_look giờ chờ trang ngừng thay đổi (dùng MutationObserver, 250 ms yên tĩnh / giới hạn 2000 ms) thay vì chờ timeout networkidle mà cart island liên tục polling và các kiểm tra session của storefront không bao giờ thỏa mãn được, rút ngắn một lần chụp điển hình từ ~3.2s xuống dưới 1s, đồng thời vẫn chụp ảnh một trang không bao giờ ổn định thay vì giữ mãi không chụp.
13
+
14
+ ## [0.4.1] - 2026-09-08
15
+
16
+ ### Fixed
17
+ - sb_review giờ báo cáo các finding bên trong cart drawer và các overlay khác của site, gắn cờ overlay: true, thay vì âm thầm bỏ qua chúng vì cho rằng một overlay không phải trách nhiệm của trang; thông báo review giải thích rằng một finding có overlay: true được sửa theo cùng cách nhưng có phạm vi toàn site, nên chỉ cần sửa một lần.
18
+ - path_params của sb_api_call giờ khớp tên tham số không phân biệt hoa thường như một phương án dự phòng, nên một lời gọi dùng cách viết phổ biến siteId không còn thất bại với 8 operation viết là siteID; một khớp chính xác vẫn được ưu tiên và một tham số thực sự thiếu vẫn bị từ chối.
19
+
9
20
  ## [0.4.0] - 2026-09-08
10
21
 
11
22
  ### Added
@@ -12,7 +12,9 @@ import { fill } from './findings.js';
12
12
  export const REVIEW_NOTICE = 'FIX THESE. Each one is a defect a visitor will see on the published page, not a ' +
13
13
  'suggestion — a blank band, a placeholder sentence, a broken image. Apply the fix each ' +
14
14
  'finding names, then review again until the list is empty. Do not report the page as done ' +
15
- 'while findings stand; if you believe one is a false positive, say which and why.';
15
+ 'while findings stand; if you believe one is a false positive, say which and why. ' +
16
+ 'A finding marked overlay:true is in the cart drawer or a pop-up — fix it the same way ' +
17
+ '(sb_set lands there), but it is SITE-WIDE, so fix it once rather than once per page.';
16
18
  /** The specials keys an element seeds that hold its visible content. */
17
19
  function contentKeys(type) {
18
20
  const seeded = ELEMENTS[type]?.defaults.specials ?? {};
@@ -56,8 +58,19 @@ function drawsItsOwnContent(type) {
56
58
  /**
57
59
  * Everything wrong with this page that a person would notice.
58
60
  *
59
- * Overlays are skipped: the cart drawer is composed onto ROOT on read and is not
60
- * this page's to fix. So is the INSIDE of an app block (trap 5): its
61
+ * OVERLAYS ARE WALKED, and used not to be on the stated reasoning that the
62
+ * cart drawer "is not this page's to fix", which is false. An overlay's content
63
+ * reaches storage THROUGH THE PAGE SAVE (`overlays/rest/rest.go`: content is
64
+ * "deliberately NOT written here"), so `sb_set` on a drawer node lands, and the
65
+ * skip meant nothing ever reported what shipped inside one. Measured: a
66
+ * rose-and-ink storefront whose drawer carried a static mock row reading
67
+ * "Product name / 0₫", a duplicate cart list, and English copy — none of it
68
+ * mentioned by any check, on a site that reviewed clean ten pages running.
69
+ *
70
+ * Their findings carry `overlay: true`, because the master is SHARED: without
71
+ * the flag the same drawer defect reads as ten problems on a ten-page site.
72
+ *
73
+ * The INSIDE of an app block is still skipped (trap 5): its
61
74
  * placeholders are the app's, and no fix this page could apply would survive a
62
75
  * save. The block root itself is still walked — it is a node the page owns and
63
76
  * can be an empty container. Findings are ordered by document order so a caller working
@@ -84,10 +97,14 @@ export function reviewDesign(doc) {
84
97
  // itself belongs to the scope OUTSIDE it, which is why this is set before the
85
98
  // scope for the children is computed.
86
99
  const inRepeater = new Map();
87
- const go = (id, repeater) => {
88
- if (seen.has(id) || overlayIds.has(id))
100
+ // Which nodes sit inside an overlay, so their findings can say so.
101
+ const inOverlay = new Set();
102
+ const go = (id, repeater, overlay = false) => {
103
+ if (seen.has(id))
89
104
  return;
90
105
  seen.add(id);
106
+ if (overlay)
107
+ inOverlay.add(id);
91
108
  walkOrder.push(id);
92
109
  if (repeater)
93
110
  inRepeater.set(id, repeater);
@@ -95,7 +112,7 @@ export function reviewDesign(doc) {
95
112
  return;
96
113
  const inner = repeats(d.nodes[id]?.data.type ?? '') ? (repeater ?? id) : repeater;
97
114
  for (const k of childrenOf(d, id))
98
- go(k, inner);
115
+ go(k, inner, overlay || overlayIds.has(k));
99
116
  };
100
117
  go(d.root_node_id);
101
118
  for (const id of walkOrder) {
@@ -301,5 +318,12 @@ export function reviewDesign(doc) {
301
318
  }
302
319
  }
303
320
  }
321
+ // Tagged in ONE place rather than at eight push sites: whether a node sits
322
+ // inside an overlay is a fact about where it is, not about what is wrong with
323
+ // it, and threading it through every rule would put the same argument in eight
324
+ // signatures.
325
+ for (const f of out)
326
+ if (inOverlay.has(f.nodeId))
327
+ f.overlay = true;
304
328
  return out;
305
329
  }
package/dist/tools/api.js CHANGED
@@ -171,7 +171,19 @@ export async function callOperation(ctx, args) {
171
171
  let path = op.path;
172
172
  for (const m of op.path.matchAll(/\{([^}]+)\}/g)) {
173
173
  const name = m[1];
174
- const value = args.path_params?.[name];
174
+ // CASE-INSENSITIVE, because the platform spells one parameter two ways:
175
+ // `{siteId}` in 281 operations and `{siteID}` in 8. A caller who learned the
176
+ // common spelling passes the wrong key on those eight and is refused for a
177
+ // difference of one letter — a distinction no reader of the call sheet has
178
+ // any reason to notice, and one this tool has nothing to gain by enforcing.
179
+ // The exact name still wins; the fold is only a fallback.
180
+ const given = args.path_params ?? {};
181
+ let value = given[name];
182
+ if (value === undefined) {
183
+ const folded = Object.keys(given).find((k) => k.toLowerCase() === name.toLowerCase());
184
+ if (folded !== undefined)
185
+ value = given[folded];
186
+ }
175
187
  if (value === undefined) {
176
188
  // NAME THE ARGUMENT, not just the parameter. The call sheet lists these
177
189
  // under `params` while the call takes them in `path_params`, and a caller
@@ -138,6 +138,54 @@ export async function shoot(url, opts = {}) {
138
138
  }
139
139
  }));
140
140
  }
141
+ /**
142
+ * WAIT FOR THE PAGE TO STOP CHANGING, not for the network to go quiet.
143
+ *
144
+ * This used to be `waitForLoadState('networkidle', { timeout: 2_500 })`, with a
145
+ * comment explaining that a storefront never goes idle — the cart island polls,
146
+ * a session endpoint answers 401 forever. That was correct, and it meant the
147
+ * wait ALWAYS ran to its cap: measured at 2502 ms on every single look, three
148
+ * runs out of three, against a 2847 ms total. Eighty-eight per cent of a
149
+ * screenshot was a timeout the code already knew would never resolve, paid after
150
+ * every edit of a vision loop.
151
+ *
152
+ * A MutationObserver answers the question actually being asked — has the page
153
+ * finished rendering — and answers it the moment it is true. Measured on the
154
+ * same three pages: 400-460 ms, with identical content on screen (images,
155
+ * prices, no empty states). The storefront renders its lists SERVER-side, so
156
+ * everything is present a few hundred ms after `load`; the old wait bought
157
+ * nothing but latency.
158
+ *
159
+ * Bounded twice over, and both bounds matter: `quiet` is how long nothing may
160
+ * change before the page counts as settled, and `cap` stops an animation or a
161
+ * polling widget from holding the shot forever. A page that never settles is
162
+ * photographed anyway — a late picture beats none.
163
+ */
164
+ async function settleDom(page) {
165
+ await page
166
+ .evaluate(({ quiet, cap }) => new Promise((resolve) => {
167
+ const start = Date.now();
168
+ let last = Date.now();
169
+ const mo = new MutationObserver(() => {
170
+ last = Date.now();
171
+ });
172
+ mo.observe(document.documentElement, {
173
+ subtree: true,
174
+ childList: true,
175
+ attributes: true,
176
+ characterData: true,
177
+ });
178
+ const tick = setInterval(() => {
179
+ const now = Date.now();
180
+ if (now - last >= quiet || now - start >= cap) {
181
+ clearInterval(tick);
182
+ mo.disconnect();
183
+ resolve();
184
+ }
185
+ }, 50);
186
+ }), { quiet: 250, cap: 2_000 })
187
+ .catch(() => { });
188
+ }
141
189
  /**
142
190
  * WALK THE PAGE so its lazy images load, then come back to the top.
143
191
  *
@@ -187,7 +235,7 @@ async function shootOne(page, url, width, format, opts) {
187
235
  // storefront. The settle is best-effort: if the page does go quiet, the shot
188
236
  // waits for it; if it never does, the shot happens anyway.
189
237
  await page.goto(url, { waitUntil: 'load', timeout: 30_000 });
190
- await page.waitForLoadState('networkidle', { timeout: 2_500 }).catch(() => { });
238
+ await settleDom(page);
191
239
  await settleLazyImages(page);
192
240
  // A RENDERED page carries its node ids as the HTML `id` attribute — not as
193
241
  // `data-node-id`, which is the editor CANVAS's hook and never reaches the
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sbuilder-mcp",
3
- "version": "0.4.0",
3
+ "version": "0.4.2",
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",