sbuilder-mcp 0.9.2 → 0.10.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 +8 -0
- package/CHANGELOG.vi.md +8 -0
- package/dist/domains/site/builder.js +31 -7
- package/dist/domains/site/review.js +14 -2
- package/dist/tools/page.js +13 -4
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,14 @@ 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.10.0] - 2026-09-09
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
- sb_set now takes `unset`, an array of key names removed from the same slot a write would target — base, a breakpoint, or either home of a state — giving every finding that names "remove the override" as its fix a tool that can actually perform it. `keys` is now optional when `unset` carries the work, and a pure removal is exempt from the sticky-host guard so it can repair a `stuck_no_host` finding rather than being refused by it.
|
|
13
|
+
|
|
14
|
+
### Fixed
|
|
15
|
+
- sb_review's `stuck_no_host` check now counts the keys inside a stuck state slot instead of only checking whether the slot exists, matching the platform's own `HasStuckOverrides`; an empty slot (the state a repair with `unset` leaves behind) no longer keeps reporting the finding it was used to fix.
|
|
16
|
+
|
|
9
17
|
## [0.9.2] - 2026-09-09
|
|
10
18
|
|
|
11
19
|
### Added
|
package/CHANGELOG.vi.md
CHANGED
|
@@ -6,6 +6,14 @@ 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.10.0] - 2026-09-09
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
- sb_set giờ nhận `unset`, một mảng tên key cần xoá khỏi đúng slot mà một lần ghi sẽ nhắm tới — base, một breakpoint, hoặc một trong hai nơi lưu của một state — nhờ đó mọi finding nêu "xoá override đi" làm cách sửa giờ có công cụ để thực sự làm điều đó. `keys` giờ có thể bỏ qua khi `unset` đã làm phần việc, và một lần xoá thuần tuý được miễn khỏi kiểm tra sticky-host để nó có thể sửa một finding `stuck_no_host` thay vì bị chính kiểm tra đó từ chối.
|
|
13
|
+
|
|
14
|
+
### Fixed
|
|
15
|
+
- Kiểm tra `stuck_no_host` của sb_review giờ đếm số key bên trong slot trạng thái stuck thay vì chỉ kiểm tra slot có tồn tại hay không, khớp với `HasStuckOverrides` của chính nền tảng; một slot rỗng (trạng thái mà một lần sửa bằng `unset` để lại) không còn tiếp tục báo cáo finding mà nó vừa được dùng để khắc phục.
|
|
16
|
+
|
|
9
17
|
## [0.9.2] - 2026-09-09
|
|
10
18
|
|
|
11
19
|
### Added
|
|
@@ -188,6 +188,21 @@ export function setKeys(doc, id, keys, opts) {
|
|
|
188
188
|
// a breakpoint, never inside a stuck slot.
|
|
189
189
|
if (namespace === 'config')
|
|
190
190
|
refuseStuckAfter(doc.doc, id, keys);
|
|
191
|
+
// The paths `keys` and `unset` share. Computed once so a removal can never
|
|
192
|
+
// land somewhere a write would not have.
|
|
193
|
+
const slot = () => {
|
|
194
|
+
if (namespace === 'specials')
|
|
195
|
+
return ['nodes', id, 'specials'];
|
|
196
|
+
if (opts.state) {
|
|
197
|
+
return opts.base
|
|
198
|
+
? ['nodes', id, 'states', opts.state, namespace]
|
|
199
|
+
: ['nodes', id, 'responsive', opts.breakpoint ?? 'desktop', 'states', opts.state, namespace];
|
|
200
|
+
}
|
|
201
|
+
if (opts.base)
|
|
202
|
+
return ['nodes', id, namespace];
|
|
203
|
+
return ['nodes', id, 'responsive', opts.breakpoint ?? 'desktop', namespace];
|
|
204
|
+
};
|
|
205
|
+
const removals = (opts.unset ?? []).map((k) => ({ op: 'unset', path: [...slot(), k] }));
|
|
191
206
|
if (namespace === 'specials') {
|
|
192
207
|
refuseComposedStamp(keys);
|
|
193
208
|
// `specials` is content and identity, base-only by definition, and states
|
|
@@ -198,11 +213,14 @@ export function setKeys(doc, id, keys, opts) {
|
|
|
198
213
|
'specials is content and identity (text, htmlTag, bound…), which do not vary by ' +
|
|
199
214
|
'state. Style is what has states.');
|
|
200
215
|
}
|
|
201
|
-
return
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
216
|
+
return [
|
|
217
|
+
...Object.entries(keys).map(([k, v]) => ({
|
|
218
|
+
op: 'set',
|
|
219
|
+
path: ['nodes', id, 'specials', k],
|
|
220
|
+
value: v,
|
|
221
|
+
})),
|
|
222
|
+
...removals,
|
|
223
|
+
];
|
|
206
224
|
}
|
|
207
225
|
// A STATE IS ANSWERED BEFORE `base`, because `base` used to be tested first
|
|
208
226
|
// and swallowed it: `base:true state:"hover"` wrote the hover value straight
|
|
@@ -216,7 +234,10 @@ export function setKeys(doc, id, keys, opts) {
|
|
|
216
234
|
// class a runtime island toggles on the PINNED element, and the renderer
|
|
217
235
|
// emits no rule at all when there is nothing pinned to hang it off. See
|
|
218
236
|
// sticky.ts — this is the whole reason that module exists.
|
|
219
|
-
|
|
237
|
+
// A pure REMOVAL is exempt: unsetting a hostless stuck override is exactly
|
|
238
|
+
// the repair `sb_review` asks for, and refusing it would leave the caller
|
|
239
|
+
// holding a finding they cannot act on.
|
|
240
|
+
if (opts.state === STUCK_STATE && Object.keys(keys).length) {
|
|
220
241
|
requireStuckHost(doc.doc, id);
|
|
221
242
|
if (namespace === 'config')
|
|
222
243
|
refuseStuckConfig(keys);
|
|
@@ -233,6 +254,7 @@ export function setKeys(doc, id, keys, opts) {
|
|
|
233
254
|
path: [...prefix, namespace, k],
|
|
234
255
|
value: v,
|
|
235
256
|
})),
|
|
257
|
+
...removals,
|
|
236
258
|
...(rebind ? [rebind] : []),
|
|
237
259
|
];
|
|
238
260
|
}
|
|
@@ -270,6 +292,7 @@ export function setKeys(doc, id, keys, opts) {
|
|
|
270
292
|
path: ['nodes', id, namespace, k],
|
|
271
293
|
value: v,
|
|
272
294
|
})),
|
|
295
|
+
...removals,
|
|
273
296
|
...(rebind ? [rebind] : []),
|
|
274
297
|
];
|
|
275
298
|
}
|
|
@@ -280,6 +303,7 @@ export function setKeys(doc, id, keys, opts) {
|
|
|
280
303
|
path: ['nodes', id, 'responsive', bp, namespace, k],
|
|
281
304
|
value: v,
|
|
282
305
|
})),
|
|
306
|
+
...removals,
|
|
283
307
|
...(rebind ? [rebind] : []),
|
|
284
308
|
];
|
|
285
309
|
}
|
|
@@ -492,7 +516,7 @@ export function setMany(doc, edits) {
|
|
|
492
516
|
const patches = [];
|
|
493
517
|
const touched = [];
|
|
494
518
|
for (const e of edits) {
|
|
495
|
-
patches.push(...setKeys(doc, e.id, e.keys, { namespace: e.namespace, breakpoint: e.breakpoint, base: e.base, state: e.state }));
|
|
519
|
+
patches.push(...setKeys(doc, e.id, e.keys, { namespace: e.namespace, breakpoint: e.breakpoint, base: e.base, state: e.state, unset: e.unset }));
|
|
496
520
|
touched.push({ id: e.id, keys: Object.keys(e.keys) });
|
|
497
521
|
}
|
|
498
522
|
return { patches, touched };
|
|
@@ -214,8 +214,20 @@ export function reviewDesign(doc) {
|
|
|
214
214
|
});
|
|
215
215
|
}
|
|
216
216
|
}
|
|
217
|
-
|
|
218
|
-
|
|
217
|
+
// COUNT THE KEYS, don't ask whether the slot exists — the platform's own
|
|
218
|
+
// `HasStuckOverrides` tests `len(slot.Style) > 0`, and an EMPTY slot paints
|
|
219
|
+
// nothing whether or not there is a host. Asking the weaker question made
|
|
220
|
+
// this a false positive on the one document the fix produces: unsetting the
|
|
221
|
+
// last key leaves `{}` behind, so the repair the finding names left the
|
|
222
|
+
// finding standing.
|
|
223
|
+
const stuckSlots = [
|
|
224
|
+
styled.states?.[STUCK_STATE],
|
|
225
|
+
...Object.values(styled.responsive ?? {}).map((r) => r?.states?.[STUCK_STATE]),
|
|
226
|
+
];
|
|
227
|
+
const hasStuck = stuckSlots.some((slot) => {
|
|
228
|
+
const s = slot;
|
|
229
|
+
return !!s && (Object.keys(s.style ?? {}).length > 0 || Object.keys(s.config ?? {}).length > 0);
|
|
230
|
+
});
|
|
219
231
|
if (hasStuck && !stuckHostOf(d, id)) {
|
|
220
232
|
out.push({
|
|
221
233
|
code: 'stuck_no_host',
|
package/dist/tools/page.js
CHANGED
|
@@ -262,6 +262,11 @@ export function registerPageTools(server, ctx) {
|
|
|
262
262
|
id: z.string().optional(),
|
|
263
263
|
namespace: z.enum(['style', 'config', 'specials']).optional(),
|
|
264
264
|
keys: z.record(z.unknown()).optional(),
|
|
265
|
+
unset: z
|
|
266
|
+
.array(z.string())
|
|
267
|
+
.optional()
|
|
268
|
+
.describe('Keys to REMOVE from the same slot — the only way to undo a write. Setting null ' +
|
|
269
|
+
'is not the same: null is a stored value, so the override still counts as present.'),
|
|
265
270
|
breakpoint: z.enum(['desktop', 'laptop', 'tablet', 'mobile']).optional(),
|
|
266
271
|
base: z.boolean().optional(),
|
|
267
272
|
state: z
|
|
@@ -278,20 +283,24 @@ export function registerPageTools(server, ctx) {
|
|
|
278
283
|
breakpoint: z.enum(['desktop', 'laptop', 'tablet', 'mobile']).optional(),
|
|
279
284
|
base: z.boolean().optional(),
|
|
280
285
|
state: z.string().optional(),
|
|
286
|
+
unset: z.array(z.string()).optional(),
|
|
281
287
|
}))
|
|
282
288
|
.optional(),
|
|
283
289
|
dry_run: z.boolean().optional(),
|
|
284
290
|
},
|
|
285
291
|
annotations: { readOnlyHint: false, destructiveHint: false },
|
|
286
|
-
}, async ({ id, namespace, keys, breakpoint, base, state, edits, dry_run }) => {
|
|
292
|
+
}, async ({ id, namespace, keys, breakpoint, base, state, unset, edits, dry_run }) => {
|
|
287
293
|
const d = session.current();
|
|
288
294
|
// One shape inside: a single edit is a batch of one.
|
|
289
295
|
const batch = edits ?? [];
|
|
290
296
|
if (!edits) {
|
|
291
|
-
|
|
292
|
-
|
|
297
|
+
// `keys` is optional when `unset` carries the work: a pure removal is a
|
|
298
|
+
// legitimate edit, and demanding an empty object alongside it would make
|
|
299
|
+
// the repair `sb_review` names read like a workaround.
|
|
300
|
+
if (!id || !namespace || (!keys && !unset?.length)) {
|
|
301
|
+
throw new Error('sbuilder: sb_set needs id + namespace + keys (or unset), or edits[]');
|
|
293
302
|
}
|
|
294
|
-
batch.push({ id, namespace, keys, breakpoint: breakpoint, base, state });
|
|
303
|
+
batch.push({ id, namespace, keys: keys ?? {}, breakpoint: breakpoint, base, state, unset });
|
|
295
304
|
}
|
|
296
305
|
const { patches, touched } = setMany(d, batch);
|
|
297
306
|
// THE STICKY WARNING IS COMPUTED AGAINST THE DOCUMENT AS IT WILL BE, so
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sbuilder-mcp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.10.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",
|