sbuilder-mcp 0.30.1 → 0.32.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 +15 -0
- package/CHANGELOG.vi.md +15 -0
- package/dist/catalog/api.generated.js +2 -2
- package/dist/catalog/search.js +28 -0
- package/dist/domains/site/importmap.js +23 -0
- package/dist/vision/capture.js +125 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,21 @@ 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.32.0] - 2026-09-11
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
- sb_api_find's call sheet for any `/apps` or `/builtin-apps` operation now explains how to place an installed app's block on a page: build `specials.appBlockRef` as `"<installId>/<blockKey>"` from a row of `GET /api/sites/{siteId}/apps/blocks` and add it with sb_add, which was previously reachable only in the platform's Go source.
|
|
13
|
+
- The same call sheet names the two silent traps around a placed block: never author `specials.appBlockId` or `appBlockHash` (the server's own composition stamps), and an edit made inside a composed block's subtree is stored nowhere.
|
|
14
|
+
- The call sheet also explains installing an app: a built-in app installs with `POST /api/sites/{siteId}/builtin-apps/{key}`, while a marketplace app requires a merchant to approve an OAuth consent screen and cannot be installed through this server.
|
|
15
|
+
|
|
16
|
+
### Fixed
|
|
17
|
+
- The `key` parameter on the built-in app install and list operations now documents all eight installable keys (mail, multilingual, agent, chat, booking, loyalty, payments, courses) instead of only the first two ever shipped.
|
|
18
|
+
|
|
19
|
+
## [0.31.0] - 2026-09-11
|
|
20
|
+
|
|
21
|
+
### Added
|
|
22
|
+
- sb_import and sb_import_site now turn a set of panels with a button row into a real `tab` element instead of flattening it, as long as every panel's label can be read off the page (via `aria-controls`, a shared `data-*` pairing, or position when the button row's clickable count matches the panel count); a panel set whose labels are filled in by script rather than present in the DOM is reported as `panel-set-without-labels` and left flattened rather than guessed at.
|
|
23
|
+
|
|
9
24
|
## [0.30.1] - 2026-09-11
|
|
10
25
|
|
|
11
26
|
### Changed
|
package/CHANGELOG.vi.md
CHANGED
|
@@ -6,6 +6,21 @@ 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.32.0] - 2026-09-11
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
- Call sheet của sb_api_find cho mọi operation `/apps` hoặc `/builtin-apps` giờ giải thích cách đặt block của một app đã cài lên trang: dựng `specials.appBlockRef` dạng `"<installId>/<blockKey>"` từ một dòng của `GET /api/sites/{siteId}/apps/blocks` rồi thêm bằng sb_add — điều trước đây chỉ có thể biết được qua mã nguồn Go của nền tảng.
|
|
13
|
+
- Cùng call sheet đó nêu rõ hai cái bẫy âm thầm quanh một block đã đặt: không bao giờ tự viết `specials.appBlockId` hay `appBlockHash` (đây là các dấu server tự gắn khi compose), và một chỉnh sửa bên trong subtree đã compose sẽ không được lưu ở đâu cả.
|
|
14
|
+
- Call sheet cũng giải thích cách cài app: một app tích hợp sẵn cài bằng `POST /api/sites/{siteId}/builtin-apps/{key}`, còn một app từ marketplace cần merchant tự duyệt màn hình xác nhận OAuth và không thể cài qua server này.
|
|
15
|
+
|
|
16
|
+
### Fixed
|
|
17
|
+
- Tham số `key` trên các operation cài và liệt kê app tích hợp sẵn giờ ghi đủ tám key có thể cài (mail, multilingual, agent, chat, booking, loyalty, payments, courses) thay vì chỉ hai key đầu tiên từng có.
|
|
18
|
+
|
|
19
|
+
## [0.31.0] - 2026-09-11
|
|
20
|
+
|
|
21
|
+
### Added
|
|
22
|
+
- sb_import và sb_import_site giờ biến một bộ panel có hàng nút thành một element `tab` thật thay vì làm phẳng nó, miễn là nhãn của mọi panel đọc được trên trang (qua `aria-controls`, một cặp giá trị `data-*` chung, hoặc theo vị trí khi số phần tử bấm được trong hàng nút khớp với số panel); một bộ panel có nhãn do script điền vào chứ không có sẵn trong DOM sẽ được báo là `panel-set-without-labels` và giữ nguyên dạng làm phẳng thay vì bị đoán mò.
|
|
23
|
+
|
|
9
24
|
## [0.30.1] - 2026-09-11
|
|
10
25
|
|
|
11
26
|
### Changed
|
|
@@ -3971,7 +3971,7 @@ export const API_OPERATIONS = [
|
|
|
3971
3971
|
"in": "path",
|
|
3972
3972
|
"required": true,
|
|
3973
3973
|
"type": "string",
|
|
3974
|
-
"description": "App key
|
|
3974
|
+
"description": "App key: mail | multilingual | agent | chat | booking | loyalty | payments | courses"
|
|
3975
3975
|
}
|
|
3976
3976
|
],
|
|
3977
3977
|
"bodyDescribed": false,
|
|
@@ -3999,7 +3999,7 @@ export const API_OPERATIONS = [
|
|
|
3999
3999
|
"in": "path",
|
|
4000
4000
|
"required": true,
|
|
4001
4001
|
"type": "string",
|
|
4002
|
-
"description": "App key
|
|
4002
|
+
"description": "App key: mail | multilingual | agent | chat | booking | loyalty | payments | courses"
|
|
4003
4003
|
}
|
|
4004
4004
|
],
|
|
4005
4005
|
"bodyDescribed": false,
|
package/dist/catalog/search.js
CHANGED
|
@@ -91,6 +91,34 @@ export function describeOperation(op) {
|
|
|
91
91
|
if (/\/translations(\/|$)/.test(op.path)) {
|
|
92
92
|
out.translation_fields = translationCallSheet();
|
|
93
93
|
}
|
|
94
|
+
// AN APP'S BLOCKS WERE REACHABLE AND UNUSABLE, because the one thing needed to
|
|
95
|
+
// place one is a string format that exists only in Go. `page/appblocks.go`
|
|
96
|
+
// spells it out — SpecAppBlockRef is "<installId>/<blockKey>" — and every
|
|
97
|
+
// field to build it comes back from `/apps/blocks` (`installId`, `key`). An
|
|
98
|
+
// agent had the list of blocks, the route that returns it, and no way to turn
|
|
99
|
+
// a row into a node.
|
|
100
|
+
//
|
|
101
|
+
// Attached to the call sheet for the same reason the translation table is:
|
|
102
|
+
// that is where the agent already is when it decides what to send. No new
|
|
103
|
+
// tool — placing one is `sb_add`, which already takes the specials it needs.
|
|
104
|
+
if (/\/(apps|builtin-apps)(\/|$)/.test(op.path)) {
|
|
105
|
+
out.app_blocks = {
|
|
106
|
+
place: 'A block from an installed app goes on a page as ONE node carrying ' +
|
|
107
|
+
'specials.appBlockRef = "<installId>/<blockKey>" — both fields come back from ' +
|
|
108
|
+
'GET /api/sites/{siteId}/apps/blocks. Add it with sb_add; the platform composes the ' +
|
|
109
|
+
"app's markup underneath on read.",
|
|
110
|
+
never: 'Never author specials.appBlockId or appBlockHash. Those are the stamps the SERVER ' +
|
|
111
|
+
'writes when it composes, and writing one makes the next save decompose your node over ' +
|
|
112
|
+
'the app instead.',
|
|
113
|
+
interior: 'An edit INSIDE a composed block is stored nowhere and reported nowhere — the save ' +
|
|
114
|
+
'reduces the subtree back to the reference. Configure the block through its own ' +
|
|
115
|
+
'props/slots, never by editing what it rendered.',
|
|
116
|
+
installing: 'A BUILT-IN app installs with POST /api/sites/{siteId}/builtin-apps/{key} and that key ' +
|
|
117
|
+
"parameter's description names every installable one. A MARKETPLACE app cannot be " +
|
|
118
|
+
'installed from here at all: it goes through an OAuth consent screen a person has to ' +
|
|
119
|
+
'approve, so ask the merchant to install it and then read /apps/blocks again.',
|
|
120
|
+
};
|
|
121
|
+
}
|
|
94
122
|
const shape = REQUEST_SHAPES[op.id];
|
|
95
123
|
if (shape) {
|
|
96
124
|
out.body_shape = shape;
|
|
@@ -167,6 +167,29 @@ function one(c, t) {
|
|
|
167
167
|
children: body,
|
|
168
168
|
};
|
|
169
169
|
}
|
|
170
|
+
case 'tab': {
|
|
171
|
+
const items = (c.children ?? [])
|
|
172
|
+
.map((item) => one(item, t))
|
|
173
|
+
.filter((n) => n !== null);
|
|
174
|
+
// Two is the floor for a tab: one panel with a button over it is a
|
|
175
|
+
// heading the visitor cannot dismiss.
|
|
176
|
+
if (items.length < 2)
|
|
177
|
+
return null;
|
|
178
|
+
return { type: 'tab', children: items };
|
|
179
|
+
}
|
|
180
|
+
case 'tab-item': {
|
|
181
|
+
const label = c.text?.trim();
|
|
182
|
+
const body = (c.children ?? [])
|
|
183
|
+
.map((k) => one(k, t))
|
|
184
|
+
.filter((n) => n !== null);
|
|
185
|
+
// NO LABEL, NO PANEL — and this is stricter than the accordion beside it
|
|
186
|
+
// on purpose. `accordion-content` seeds its own summary, so an unlabelled
|
|
187
|
+
// one still opens; `tab` builds its BUTTON ROW from these labels, so an
|
|
188
|
+
// unlabelled panel is one the visitor has no way to reach.
|
|
189
|
+
if (!label || !body.length)
|
|
190
|
+
return null;
|
|
191
|
+
return { type: 'tab-content', specials: { label }, children: body };
|
|
192
|
+
}
|
|
170
193
|
case 'divider': {
|
|
171
194
|
return { type: 'divider', style: { width: '100%' } };
|
|
172
195
|
}
|
package/dist/vision/capture.js
CHANGED
|
@@ -211,6 +211,21 @@ function capturePage(limits) {
|
|
|
211
211
|
}
|
|
212
212
|
if (!hidden.length)
|
|
213
213
|
continue;
|
|
214
|
+
// STAMPED WHILE WE STILL KNOW. Once revealed, a panel is indistinguishable
|
|
215
|
+
// from an ordinary visible child, so the split is recorded here — and
|
|
216
|
+
// ONLY the ones that were actually hidden. A first version stamped every
|
|
217
|
+
// child that shared a tag with a visible peer, which swept in the BUTTON
|
|
218
|
+
// ROW itself: it is a `<div>` with children like the panels are, so the
|
|
219
|
+
// tab came out with its own controls as one of its panes.
|
|
220
|
+
let stamped = 0;
|
|
221
|
+
for (const k of hidden) {
|
|
222
|
+
if (!shownByTag[k.tagName] || k.children.length === 0)
|
|
223
|
+
continue;
|
|
224
|
+
k.setAttribute('data-sb-panel', '');
|
|
225
|
+
stamped += 1;
|
|
226
|
+
}
|
|
227
|
+
if (stamped >= 1)
|
|
228
|
+
parent.setAttribute('data-sb-panelset', '');
|
|
214
229
|
for (const k of hidden) {
|
|
215
230
|
// Real content only — an empty slot or a script-filled placeholder is
|
|
216
231
|
// not a panel worth showing, and revealing it costs a skip either way.
|
|
@@ -458,6 +473,116 @@ function capturePage(limits) {
|
|
|
458
473
|
skip('hidden');
|
|
459
474
|
return [];
|
|
460
475
|
}
|
|
476
|
+
// A TAB SET, WHEN — AND ONLY WHEN — ITS LABELS CAN BE READ.
|
|
477
|
+
//
|
|
478
|
+
// The platform's `tab` synthesizes its whole button row from each
|
|
479
|
+
// `tab-content` child's `specials.label` (render/nodes/tab/html.go), so a
|
|
480
|
+
// tab with no labels is a stack of panels wearing a control nobody can
|
|
481
|
+
// use. That makes "are there labels" the right question AND the right
|
|
482
|
+
// discriminator: MEASURED on ttgshop.vn, the panel-set stamp also lands on
|
|
483
|
+
// a `grid-cols-5` product grid that happens to hide one card, and a label
|
|
484
|
+
// row is exactly what that grid does not have.
|
|
485
|
+
//
|
|
486
|
+
// Three pairings, strongest first, because a wrong label is worse than
|
|
487
|
+
// none — it puts one collection's name over another's products:
|
|
488
|
+
// 1. ARIA. `[role=tab][aria-controls=<panel id>]` says it outright.
|
|
489
|
+
// 2. A SHARED data-* VALUE. `<button data-id="7">` beside
|
|
490
|
+
// `<div data-id="7">` is how most hand-rolled tab scripts wire
|
|
491
|
+
// themselves, and it survives reordering.
|
|
492
|
+
// 3. POSITION, and only from a row that holds exactly as many
|
|
493
|
+
// clickable items as there are panels — anything else is a guess
|
|
494
|
+
// dressed as a match.
|
|
495
|
+
//
|
|
496
|
+
// Every panel must come out with a label or this is not a tab. ttgshop.vn
|
|
497
|
+
// itself does NOT become one: its tab buttons are empty in the DOM and
|
|
498
|
+
// filled by script, and its section titles are images. Deriving a label
|
|
499
|
+
// from the `data-url` slug was available and refused — "hot-sale" is not
|
|
500
|
+
// what the page says, and a Vietnamese slug comes back stripped of its
|
|
501
|
+
// diacritics, which is the invented-copy defect this repo reports on
|
|
502
|
+
// everybody else's seeds.
|
|
503
|
+
const tabSetOf = (host) => {
|
|
504
|
+
const kids = Array.from(host.children);
|
|
505
|
+
const revealed = kids.filter((c) => c.getAttribute('data-sb-panel') !== null);
|
|
506
|
+
if (revealed.length === 0)
|
|
507
|
+
return null;
|
|
508
|
+
// THE BUTTON ROW IS A SIBLING OF THE PANELS, and it has to be taken out
|
|
509
|
+
// of the set before anything is counted — it is a container of the same
|
|
510
|
+
// shape, and leaving it in makes the panel count wrong and the pairing
|
|
511
|
+
// meaningless. It is the child that holds the clickable words.
|
|
512
|
+
// INNERMOST ONLY. `<li><button>Một</button></li>` is the ordinary way a
|
|
513
|
+
// tab bar is written, and counting both gives four clickables for two
|
|
514
|
+
// panels — which fails the count test that position pairing depends on,
|
|
515
|
+
// so a perfectly labelled tab bar read as unlabelled.
|
|
516
|
+
const clicksIn = (c) => {
|
|
517
|
+
const all = Array.from(c.querySelectorAll('button, a, li, [role="tab"]')).filter((b) => clean(b.textContent).length > 0);
|
|
518
|
+
return all.filter((b) => !all.some((o) => o !== b && b.contains(o)));
|
|
519
|
+
};
|
|
520
|
+
let bar = null;
|
|
521
|
+
for (const c of kids) {
|
|
522
|
+
if (c.getAttribute('data-sb-panel') !== null)
|
|
523
|
+
continue;
|
|
524
|
+
if (clicksIn(c).length >= 2) {
|
|
525
|
+
bar = c;
|
|
526
|
+
break;
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
const tagOf = revealed[0].tagName;
|
|
530
|
+
const panels = kids.filter((c) => c !== bar && c.tagName === tagOf && c.children.length > 0);
|
|
531
|
+
if (panels.length < 2)
|
|
532
|
+
return null;
|
|
533
|
+
const clickable = bar ? clicksIn(bar) : [];
|
|
534
|
+
const labels = [];
|
|
535
|
+
for (let i = 0; i < panels.length; i += 1) {
|
|
536
|
+
const panel = panels[i];
|
|
537
|
+
const id = panel.getAttribute('id');
|
|
538
|
+
let label = '';
|
|
539
|
+
if (id) {
|
|
540
|
+
for (const b of Array.from(host.querySelectorAll(`[aria-controls="${id}"]`))) {
|
|
541
|
+
const t = clean(b.textContent);
|
|
542
|
+
if (t) {
|
|
543
|
+
label = t;
|
|
544
|
+
break;
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
}
|
|
548
|
+
if (!label) {
|
|
549
|
+
for (const attr of Array.from(panel.attributes ?? [])) {
|
|
550
|
+
if (attr.name.indexOf('data-') !== 0 || attr.name === 'data-sb-panel' || !attr.value)
|
|
551
|
+
continue;
|
|
552
|
+
for (const b of clickable) {
|
|
553
|
+
if (b.getAttribute(attr.name) === attr.value) {
|
|
554
|
+
label = clean(b.textContent);
|
|
555
|
+
break;
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
if (label)
|
|
559
|
+
break;
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
if (!label && clickable.length === panels.length)
|
|
563
|
+
label = clean(clickable[i].textContent);
|
|
564
|
+
if (!label)
|
|
565
|
+
return null;
|
|
566
|
+
labels.push(label);
|
|
567
|
+
}
|
|
568
|
+
const children = [];
|
|
569
|
+
for (let i = 0; i < panels.length; i += 1) {
|
|
570
|
+
const body = walk(panels[i]);
|
|
571
|
+
if (body.length === 0)
|
|
572
|
+
continue;
|
|
573
|
+
children.push({ kind: 'tab-item', text: labels[i], children: body });
|
|
574
|
+
}
|
|
575
|
+
if (children.length < 2)
|
|
576
|
+
return null;
|
|
577
|
+
taken.nodes++;
|
|
578
|
+
return { kind: 'tab', children };
|
|
579
|
+
};
|
|
580
|
+
if (el.getAttribute('data-sb-panelset') !== null) {
|
|
581
|
+
const asTabs = tabSetOf(el);
|
|
582
|
+
if (asTabs)
|
|
583
|
+
return [asTabs];
|
|
584
|
+
skip('panel-set-without-labels');
|
|
585
|
+
}
|
|
461
586
|
if (tag === 'DETAILS') {
|
|
462
587
|
const kids2 = Array.from(el.children);
|
|
463
588
|
const summary = kids2.filter((c) => c.tagName === 'SUMMARY')[0];
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sbuilder-mcp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.32.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",
|