sbuilder-mcp 0.17.0 → 0.17.1

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,13 @@ 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.17.1] - 2026-09-10
10
+
11
+ ### Fixed
12
+ - sb_add and sb_import no longer store a nested node's children twice: a patch batch that gets applied more than once (as every write already is, to validate it before it lands) mutated itself on the first pass by carrying an added node by reference, so the second pass re-inserted its children into a node that already held them.
13
+ - sb_import and sb_import_site now cap a flattened row at 12 columns and treat a grid container as wrapping rather than single-line, so a page whose content wrapper is a CSS grid (a documentation site, for example) no longer imports as one row of hundreds of slivered columns.
14
+ - sb_import and sb_import_site now capture a `<pre>`/`<code>` block as one node instead of one node per syntax-highlighting `<span>`, so a code sample no longer arrives broken into dozens of single-token fragments.
15
+
9
16
  ## [0.17.0] - 2026-09-10
10
17
 
11
18
  ### Added
package/CHANGELOG.vi.md CHANGED
@@ -6,6 +6,13 @@ 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.17.1] - 2026-09-10
10
+
11
+ ### Fixed
12
+ - sb_add và sb_import giờ không còn lưu nhân đôi các con của một node lồng nhau: một batch patch bị áp dụng nhiều hơn một lần (như mọi lần ghi vẫn vậy, để kiểm tra trước khi áp thật) trước đây tự làm thay đổi chính nó ngay ở lượt đầu vì mang theo một node vừa thêm bằng tham chiếu, khiến lượt thứ hai chèn lại các con vào một node đã sẵn có chúng.
13
+ - sb_import và sb_import_site giờ giới hạn một row được làm phẳng ở tối đa 12 cột và coi container dạng grid là tự xuống dòng thay vì nằm trên một hàng, để một trang có content wrapper là CSS grid (chẳng hạn một trang tài liệu kỹ thuật) không còn bị import thành một row hàng trăm cột mỏng dính.
14
+ - sb_import và sb_import_site giờ lấy một khối `<pre>`/`<code>` thành một node duy nhất thay vì một node cho mỗi `<span>` tô cú pháp, để một đoạn code không còn bị vỡ thành hàng chục mảnh một-token-một-node.
15
+
9
16
  ## [0.17.0] - 2026-09-10
10
17
 
11
18
  ### Added
@@ -78,6 +78,16 @@ function parentOf(state, path) {
78
78
  * half-edited with nothing anywhere to say so — which is the exact failure shape
79
79
  * this whole repo exists to rule out.
80
80
  */
81
+ /**
82
+ * A patch's value, detached from whatever the caller still holds.
83
+ *
84
+ * Primitives are returned as they are — a style key is a string, and cloning
85
+ * one on every `sb_set` would be pure cost. Only a structure can be aliased,
86
+ * and only an alias can be mutated behind the batch's back.
87
+ */
88
+ function clone(value) {
89
+ return value !== null && typeof value === 'object' ? structuredClone(value) : value;
90
+ }
81
91
  export function applyPatches(state, patches) {
82
92
  for (const p of patches) {
83
93
  if (!isSyncablePatch(p)) {
@@ -86,7 +96,27 @@ export function applyPatches(state, patches) {
86
96
  const { holder, key } = parentOf(state, p.path);
87
97
  switch (p.op) {
88
98
  case 'set':
89
- holder[key] = p.value;
99
+ // THE VALUE IS NEVER ALIASED INTO THE DOCUMENT.
100
+ //
101
+ // `addSubtree` builds a node, emits `set nodes/<id>` carrying THAT
102
+ // object, and then emits `insert` patches that push child ids into
103
+ // `data.nodes`. Assigning the reference means the first apply MUTATES
104
+ // the patch's own value, so the batch is no longer the thing it was: a
105
+ // second apply re-establishes a node that already holds its children and
106
+ // then inserts them again.
107
+ //
108
+ // Applying a batch twice is not hypothetical — `applyAndSave` does it by
109
+ // design, once through `preview` to judge the write and once for real.
110
+ // Between v0.16.1 (which introduced that check) and this fix, EVERY
111
+ // nested `sb_add` stored each child twice, and `sb_import` three times.
112
+ // It is silent: the tree is well-formed, every id resolves, the save is
113
+ // accepted, and the page simply renders its content twice. Measured on
114
+ // a real import — 106 of 231 containers listing one child id three
115
+ // times.
116
+ //
117
+ // A copy makes the batch idempotent for this shape: the re-`set` puts a
118
+ // pristine node back, and the inserts that follow rebuild the same list.
119
+ holder[key] = clone(p.value);
90
120
  break;
91
121
  case 'unset':
92
122
  delete holder[key];
@@ -95,7 +125,7 @@ export function applyPatches(state, patches) {
95
125
  const arr = holder[key];
96
126
  if (!Array.isArray(arr))
97
127
  break;
98
- arr.splice(p.index, 0, p.value);
128
+ arr.splice(p.index, 0, clone(p.value));
99
129
  break;
100
130
  }
101
131
  case 'remove': {
@@ -183,6 +183,25 @@ function capturePage(limits) {
183
183
  taken.nodes++;
184
184
  return [{ kind: 'list', items }];
185
185
  }
186
+ // A CODE BLOCK IS ONE THING, and walking into it produces rubble.
187
+ //
188
+ // Every syntax highlighter wraps each token in its own <span>, so the leaf
189
+ // walk took them one at a time: a twenty-line JSON config arrived as forty
190
+ // separate text nodes — `{`, `"mcpServers"`, `: {` — each its own block on
191
+ // its own line. Measured on a real import; it also ate forty of the node
192
+ // budget to say what one node says.
193
+ //
194
+ // Whitespace is collapsed like any other text because this platform has no
195
+ // code element to preserve it in — the six kinds are what can cross — so
196
+ // the honest translation is one paragraph the merchant can then restyle,
197
+ // not a shredded imitation of a listing.
198
+ if (tag === 'PRE' || tag === 'CODE') {
199
+ const text = clean(el.textContent);
200
+ if (!text)
201
+ return [];
202
+ taken.nodes++;
203
+ return [{ kind: 'text', text }];
204
+ }
186
205
  if (tag === 'P' || tag === 'BLOCKQUOTE') {
187
206
  const text = clean(el.textContent);
188
207
  if (!text)
@@ -197,12 +216,34 @@ function capturePage(limits) {
197
216
  cs.display === 'inline-flex' || cs.display === 'inline-grid';
198
217
  // A ROW is worth keeping; a column is what the page already is, so
199
218
  // wrapping one in a group would add a level that renders identically.
200
- const row = cs.display.indexOf('grid') >= 0
201
- ? true
202
- : cs.flexDirection === 'row' || cs.flexDirection === 'row-reverse';
203
- if (lays && row && kids.length >= 2) {
219
+ const grid = cs.display.indexOf('grid') >= 0;
220
+ const row = grid || cs.flexDirection === 'row' || cs.flexDirection === 'row-reverse';
221
+ // A ROW OF 279 IS NOT A ROW.
222
+ //
223
+ // Measured on a real import of a documentation site: its content wrapper
224
+ // is a GRID, every grid was read as a single row, and the whole page came
225
+ // back as one flex row of 279 columns — each column a sliver, every line
226
+ // of prose broken to one word, and 80 nodes hanging past the viewport.
227
+ // The page was structurally perfect and visually destroyed.
228
+ //
229
+ // A design row is a feature trio, a card shelf, a logo wall: a handful of
230
+ // columns, chosen. Past that the container is not arranging things side
231
+ // by side, it is the page's own content column and the browser is
232
+ // wrapping it — so the honest translation is the stack it already reads
233
+ // as. Twelve is above any real row seen here and far below a content
234
+ // grid.
235
+ const ROW_MAX = 12;
236
+ if (lays && row && kids.length >= 2 && kids.length <= ROW_MAX) {
204
237
  taken.nodes++;
205
- return [{ kind: 'group', direction: 'row', wrap: cs.flexWrap === 'wrap', children: kids }];
238
+ return [{
239
+ kind: 'group',
240
+ direction: 'row',
241
+ // A GRID ALWAYS WRAPS — that is what a grid IS — and `flexWrap` reads
242
+ // `nowrap` on one because the property does not apply. Carrying that
243
+ // literally gave the columns nowhere to go at any width.
244
+ wrap: grid || cs.flexWrap === 'wrap',
245
+ children: kids,
246
+ }];
206
247
  }
207
248
  return kids;
208
249
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "sbuilder-mcp",
3
- "version": "0.17.0",
3
+ "version": "0.17.1",
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",