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 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 Object.entries(keys).map(([k, v]) => ({
202
- op: 'set',
203
- path: ['nodes', id, 'specials', k],
204
- value: v,
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
- if (opts.state === STUCK_STATE) {
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
- const hasStuck = styled.states?.[STUCK_STATE] !== undefined ||
218
- Object.values(styled.responsive ?? {}).some((r) => r?.states?.[STUCK_STATE] !== undefined);
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',
@@ -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
- if (!id || !namespace || !keys) {
292
- throw new Error('sbuilder: sb_set needs id + namespace + keys, or edits[]');
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.9.2",
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",