sbuilder-mcp 0.16.0 → 0.16.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 +6 -0
- package/CHANGELOG.vi.md +6 -0
- package/dist/domains/site/document.js +23 -0
- package/dist/tools/importpage.js +14 -3
- package/dist/tools/live.js +2 -4
- package/dist/tools/page.js +35 -10
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,12 @@ 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.16.1] - 2026-09-10
|
|
10
|
+
|
|
11
|
+
### Fixed
|
|
12
|
+
- sb_add, sb_set, sb_move, sb_remove, sb_duplicate, sb_event and sb_bind now judge a write against a throwaway copy of the page before applying it, so a save the platform refuses is never applied to the draft or broadcast to a live session; previously the refused node stayed in the document and every later command was validated against a tree the caller never asked for, repeating the same complaint about an id it had never typed. A write is still refused only for problems it introduces, not for damage the page already had when the session opened it.
|
|
13
|
+
- sb_import_page now builds its whole run of sections on a copy and commits them in one save, so a refusal partway through no longer leaves a page half imported with no way to tell which sections landed.
|
|
14
|
+
|
|
9
15
|
## [0.16.0] - 2026-09-10
|
|
10
16
|
|
|
11
17
|
### Added
|
package/CHANGELOG.vi.md
CHANGED
|
@@ -6,6 +6,12 @@ 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.16.1] - 2026-09-10
|
|
10
|
+
|
|
11
|
+
### Fixed
|
|
12
|
+
- sb_add, sb_set, sb_move, sb_remove, sb_duplicate, sb_event và sb_bind giờ đánh giá một lần ghi trên một bản sao tạm của trang trước khi áp dụng thật, để một lần save bị nền tảng từ chối không bao giờ được áp vào draft hay phát ra cho một phiên live đang xem; trước đây node bị từ chối vẫn ở lại trong document và mọi lệnh sau đó đều bị kiểm tra dựa trên một tree mà người gọi chưa từng yêu cầu, lặp lại cùng một lời phàn nàn về một id mà họ chưa từng gõ. Một lần ghi vẫn chỉ bị từ chối vì vấn đề do chính nó gây ra, không phải vì hư hỏng trang đã có sẵn từ trước khi phiên làm việc mở nó.
|
|
13
|
+
- sb_import_page giờ dựng toàn bộ các section trong một lượt import trên một bản sao và commit chúng trong một lần save duy nhất, để một lần từ chối giữa chừng không còn để lại một trang import dở dang mà không cách nào biết những section nào đã vào được.
|
|
14
|
+
|
|
9
15
|
## [0.16.0] - 2026-09-10
|
|
10
16
|
|
|
11
17
|
### Added
|
|
@@ -130,6 +130,29 @@ export class PageDoc {
|
|
|
130
130
|
applyPatches(this.doc, patches);
|
|
131
131
|
this.revision += 1;
|
|
132
132
|
}
|
|
133
|
+
/**
|
|
134
|
+
* What this document WOULD be with these patches applied — a throwaway copy,
|
|
135
|
+
* so the real one is untouched.
|
|
136
|
+
*
|
|
137
|
+
* It exists so a write can be judged before it is made. Applying first and
|
|
138
|
+
* validating after leaves a refused edit sitting in the draft: the platform
|
|
139
|
+
* declines the save, the node stays, and every later command is then judged
|
|
140
|
+
* against a tree the caller never asked for and cannot see. Driving the real
|
|
141
|
+
* server, that arrived as three `sb_add` calls in a row all answering with the
|
|
142
|
+
* same complaint about a node id the caller had never typed.
|
|
143
|
+
*
|
|
144
|
+
* A JSON round-trip rather than anything cleverer. A page is hundreds of KB,
|
|
145
|
+
* which is a millisecond, and it is paid only on a write that is actually
|
|
146
|
+
* being committed — never on a dry run, never on a read.
|
|
147
|
+
*/
|
|
148
|
+
preview(patches) {
|
|
149
|
+
const copy = new PageDoc(JSON.parse(JSON.stringify(this.doc)), this.revision);
|
|
150
|
+
// Carried, not re-derived: a validator that saw a different root key on the
|
|
151
|
+
// copy than on the original would be judging a different page.
|
|
152
|
+
copy.adoptedRootKey = this.adoptedRootKey;
|
|
153
|
+
copy.apply(patches);
|
|
154
|
+
return copy;
|
|
155
|
+
}
|
|
133
156
|
/**
|
|
134
157
|
* A compressed tree — id, type, name, child count, and the flags that change
|
|
135
158
|
* what a caller may safely do with a node.
|
package/dist/tools/importpage.js
CHANGED
|
@@ -121,13 +121,24 @@ export function registerImportTools(server, ctx, session) {
|
|
|
121
121
|
`Skipped: ${JSON.stringify(shot.skipped)}. A page that builds itself with scripts ` +
|
|
122
122
|
'after load, or one behind a login, reads as empty here.');
|
|
123
123
|
}
|
|
124
|
+
// STAGED ON A COPY, committed once.
|
|
125
|
+
//
|
|
126
|
+
// Each section's patch set is computed from the tree the previous one
|
|
127
|
+
// left, so they have to be applied in order — but applying them to the
|
|
128
|
+
// REAL document means a refusal at the end leaves the page half imported,
|
|
129
|
+
// with no way for the caller to tell which half. Building on a throwaway
|
|
130
|
+
// and committing the whole run through `applyAndSave` keeps the import
|
|
131
|
+
// all-or-nothing, and keeps it to one save and one live frame.
|
|
124
132
|
const added = [];
|
|
133
|
+
const all = [];
|
|
134
|
+
const staged = doc.preview([]);
|
|
125
135
|
for (const spec of specs) {
|
|
126
|
-
const { patches, ids } = addSubtree(
|
|
127
|
-
|
|
136
|
+
const { patches, ids } = addSubtree(staged, staged.doc.root_node_id, spec);
|
|
137
|
+
staged.apply(patches);
|
|
138
|
+
all.push(...patches);
|
|
128
139
|
added.push(ids[0]);
|
|
129
140
|
}
|
|
130
|
-
await session.
|
|
141
|
+
await session.applyAndSave(all);
|
|
131
142
|
return text({
|
|
132
143
|
read: shot.url,
|
|
133
144
|
added_sections: added,
|
package/dist/tools/live.js
CHANGED
|
@@ -452,8 +452,7 @@ export function registerLiveTools(server, ctx, session) {
|
|
|
452
452
|
const patches = setEvent(d, id, trigger ?? 'click', action, payload);
|
|
453
453
|
if (dry_run !== false)
|
|
454
454
|
return text({ dry_run: true, patches });
|
|
455
|
-
session.
|
|
456
|
-
await session.save();
|
|
455
|
+
await session.applyAndSave(patches);
|
|
457
456
|
return text({ node: id, trigger: trigger ?? 'click', action, rev: d.rev });
|
|
458
457
|
});
|
|
459
458
|
server.registerTool('sb_bind', {
|
|
@@ -481,8 +480,7 @@ export function registerLiveTools(server, ctx, session) {
|
|
|
481
480
|
const patches = bindNode(d, id, source, field, action);
|
|
482
481
|
if (dry_run !== false)
|
|
483
482
|
return text({ dry_run: true, patches });
|
|
484
|
-
session.
|
|
485
|
-
await session.save();
|
|
483
|
+
await session.applyAndSave(patches);
|
|
486
484
|
return text({ bound: id, source, field, ...(action ? { action } : {}), rev: d.rev });
|
|
487
485
|
});
|
|
488
486
|
}
|
package/dist/tools/page.js
CHANGED
|
@@ -101,6 +101,36 @@ export class PageSession {
|
|
|
101
101
|
this.live.cursor(box.x + box.w / 2, box.y + box.h / 2);
|
|
102
102
|
}
|
|
103
103
|
}
|
|
104
|
+
/**
|
|
105
|
+
* The only sanctioned way to write: judge, then apply, publish and save.
|
|
106
|
+
*
|
|
107
|
+
* SPLITTING THESE WAS THE BUG. Every tool used to call `applyAndPublish` and
|
|
108
|
+
* then `save()`, and a save the platform would refuse threw with the patches
|
|
109
|
+
* already in the draft — and already broadcast to anyone watching the page
|
|
110
|
+
* live. The refused node then sat there, so the NEXT command was validated
|
|
111
|
+
* against a tree the caller had never asked for and got the same complaint
|
|
112
|
+
* about an id they had never typed. Three `sb_add` calls in a row, three
|
|
113
|
+
* identical refusals, and three copies of the element quietly in the page.
|
|
114
|
+
*
|
|
115
|
+
* The check runs on a COPY (`PageDoc.preview`), so a write that cannot be
|
|
116
|
+
* stored is never applied at all — no rollback to get wrong, and no phantom
|
|
117
|
+
* frame for a peer in the room to have to un-see.
|
|
118
|
+
*
|
|
119
|
+
* IT REFUSES ONLY WHAT THIS WRITE INTRODUCES. A page that arrived broken —
|
|
120
|
+
* damage stored before this session opened it — must not become a page nobody
|
|
121
|
+
* can edit, because the edit that repairs it is also a write. Pre-existing
|
|
122
|
+
* problems are left to `save()`, which names them.
|
|
123
|
+
*/
|
|
124
|
+
async applyAndSave(patches) {
|
|
125
|
+
const d = this.current();
|
|
126
|
+
const before = new Set(validateForSave(d));
|
|
127
|
+
const introduced = validateForSave(d.preview(patches)).filter((p) => !before.has(p));
|
|
128
|
+
if (introduced.length > 0) {
|
|
129
|
+
throw new Error(`sbuilder: refusing to save — ${introduced.join(' ')}`);
|
|
130
|
+
}
|
|
131
|
+
this.applyAndPublish(patches);
|
|
132
|
+
await this.save();
|
|
133
|
+
}
|
|
104
134
|
applyRemote(patches) {
|
|
105
135
|
this.doc?.apply(patches);
|
|
106
136
|
}
|
|
@@ -287,8 +317,7 @@ export function registerPageTools(server, ctx) {
|
|
|
287
317
|
...(inert ? { inert } : {}),
|
|
288
318
|
});
|
|
289
319
|
}
|
|
290
|
-
session.
|
|
291
|
-
await session.save();
|
|
320
|
+
await session.applyAndSave(patches);
|
|
292
321
|
return text({ added: ids, rev: d.rev, ...(inert ? { inert } : {}) });
|
|
293
322
|
});
|
|
294
323
|
server.registerTool('sb_set', {
|
|
@@ -504,8 +533,7 @@ export function registerPageTools(server, ctx) {
|
|
|
504
533
|
});
|
|
505
534
|
}
|
|
506
535
|
const warnings = stuck();
|
|
507
|
-
session.
|
|
508
|
-
await session.save();
|
|
536
|
+
await session.applyAndSave(patches);
|
|
509
537
|
for (const t of touched) {
|
|
510
538
|
const w = globalWarning(d.doc, t.id);
|
|
511
539
|
if (w)
|
|
@@ -551,8 +579,7 @@ export function registerPageTools(server, ctx) {
|
|
|
551
579
|
const patches = moveNode(d, id, parent_id, index);
|
|
552
580
|
if (dry_run !== false)
|
|
553
581
|
return text({ dry_run: true, patches });
|
|
554
|
-
session.
|
|
555
|
-
await session.save();
|
|
582
|
+
await session.applyAndSave(patches);
|
|
556
583
|
return text({ moved: id, rev: d.rev });
|
|
557
584
|
});
|
|
558
585
|
server.registerTool('sb_remove', {
|
|
@@ -564,8 +591,7 @@ export function registerPageTools(server, ctx) {
|
|
|
564
591
|
const patches = removeNode(d, id);
|
|
565
592
|
if (dry_run !== false)
|
|
566
593
|
return text({ dry_run: true, removing: patches.length });
|
|
567
|
-
session.
|
|
568
|
-
await session.save();
|
|
594
|
+
await session.applyAndSave(patches);
|
|
569
595
|
return text({ removed: id, rev: d.rev });
|
|
570
596
|
});
|
|
571
597
|
server.registerTool('sb_review', {
|
|
@@ -608,8 +634,7 @@ export function registerPageTools(server, ctx) {
|
|
|
608
634
|
const { patches, ids } = duplicateNode(d, id);
|
|
609
635
|
if (dry_run !== false)
|
|
610
636
|
return text({ dry_run: true, would_copy: ids.length });
|
|
611
|
-
session.
|
|
612
|
-
await session.save();
|
|
637
|
+
await session.applyAndSave(patches);
|
|
613
638
|
return text({ duplicated: id, into: ids[0], nodes: ids.length, rev: d.rev });
|
|
614
639
|
});
|
|
615
640
|
server.registerTool('sb_templates', {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sbuilder-mcp",
|
|
3
|
-
"version": "0.16.
|
|
3
|
+
"version": "0.16.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",
|