sbuilder-mcp 0.51.0 → 0.53.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 +18 -0
- package/CHANGELOG.vi.md +18 -0
- package/dist/domains/site/inventory.js +42 -11
- package/dist/tools/store.js +84 -5
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,24 @@ 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.53.0] - 2026-09-15
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
- `sb_store action:"form"` now takes a `page_name` (and optional `headline`) that builds a page and places the seeded form on it in the same call, instead of leaving the caller to create a page, `sb_add` a form element, and `sb_set` its `formId` by hand.
|
|
13
|
+
- `sb_page_list`'s advisory for a missing login, register, forgot-password, or contact page now names the single `sb_store action:"form" template:"..." page_name:"..."` call that builds both the form and its page, instead of pointing at the form template alone.
|
|
14
|
+
|
|
15
|
+
### Changed
|
|
16
|
+
- A page created for a seeded form is a second write and never undoes the first; if the page create is refused, `sb_store` still reports the form it already made, under a `page_failed` field, rather than rolling the form back.
|
|
17
|
+
|
|
18
|
+
## [0.52.0] - 2026-09-15
|
|
19
|
+
|
|
20
|
+
### Added
|
|
21
|
+
- `sb_page_list`'s `usually_also` advisory now names a missing `faq` page.
|
|
22
|
+
- `sb_page_list`'s `usually_also` advisory now names a missing blog listing page, but only on a site that already has a `post` (article) template to list — a site with no articles is no longer told to build one.
|
|
23
|
+
|
|
24
|
+
### Changed
|
|
25
|
+
- `sb_page_list`'s `usually_also` advisory splits the single `policy` entry into `policy-delivery` (shipping and returns) and `policy-privacy` (privacy and terms of use), so a site carrying only one of the two no longer reads as having both.
|
|
26
|
+
|
|
9
27
|
## [0.51.0] - 2026-09-15
|
|
10
28
|
|
|
11
29
|
### Added
|
package/CHANGELOG.vi.md
CHANGED
|
@@ -6,6 +6,24 @@ 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.53.0] - 2026-09-15
|
|
10
|
+
|
|
11
|
+
### Added
|
|
12
|
+
- `sb_store action:"form"` giờ nhận thêm `page_name` (và `headline` tuỳ chọn) để dựng luôn một trang và đặt form vừa tạo lên đó trong cùng một lần gọi, thay vì để người gọi phải tự tạo trang, `sb_add` phần tử form rồi `sb_set` `formId` của nó.
|
|
13
|
+
- Gợi ý của `sb_page_list` khi thiếu trang đăng nhập, đăng ký, quên mật khẩu hoặc liên hệ giờ nêu đích danh một lệnh gọi duy nhất `sb_store action:"form" template:"..." page_name:"..."` để dựng cả form lẫn trang chứa nó, thay vì chỉ trỏ tới mỗi template của form.
|
|
14
|
+
|
|
15
|
+
### Changed
|
|
16
|
+
- Trang được tạo cho một form vừa dựng là một lần ghi riêng và không bao giờ huỷ lần ghi trước đó; nếu việc tạo trang bị từ chối, `sb_store` vẫn báo cáo form đã tạo thành công, kèm trường `page_failed`, thay vì xoá form đó đi.
|
|
17
|
+
|
|
18
|
+
## [0.52.0] - 2026-09-15
|
|
19
|
+
|
|
20
|
+
### Added
|
|
21
|
+
- Gợi ý `usually_also` của `sb_page_list` giờ nêu tên trang `faq` nếu site còn thiếu.
|
|
22
|
+
- Gợi ý `usually_also` của `sb_page_list` giờ nêu tên trang danh sách blog nếu site còn thiếu, nhưng chỉ khi site đã có template bài viết (`post`) để liệt kê — site chưa có bài viết nào thì không còn bị nhắc xây trang này.
|
|
23
|
+
|
|
24
|
+
### Changed
|
|
25
|
+
- Gợi ý `usually_also` của `sb_page_list` tách mục `policy` duy nhất thành `policy-delivery` (giao hàng và đổi trả) và `policy-privacy` (bảo mật và điều khoản sử dụng), để một site chỉ có một trong hai trang không còn bị đọc nhầm là đã có đủ cả hai.
|
|
26
|
+
|
|
9
27
|
## [0.51.0] - 2026-09-15
|
|
10
28
|
|
|
11
29
|
### Added
|
|
@@ -29,40 +29,71 @@ export const USUAL_PAGES = [
|
|
|
29
29
|
key: 'login',
|
|
30
30
|
match: ['login', 'signin', 'sign-in', 'dang-nhap', 'đăng nhập'],
|
|
31
31
|
why: 'Nothing for a header, an order email or /account\'s signed-out state to link to. ' +
|
|
32
|
-
'
|
|
32
|
+
'sb_store action:"form" template:"login" page_name:"Đăng nhập" builds the form AND the page it lives on.',
|
|
33
33
|
},
|
|
34
34
|
{
|
|
35
35
|
key: 'register',
|
|
36
36
|
match: ['register', 'signup', 'sign-up', 'dang-ky', 'đăng ký'],
|
|
37
37
|
why: 'A shopper cannot open an account, so order history, addresses and any member-gated ' +
|
|
38
|
-
'page are unreachable.
|
|
38
|
+
'page are unreachable. sb_store action:"form" template:"register" with a page_name.',
|
|
39
39
|
},
|
|
40
40
|
{
|
|
41
41
|
key: 'forgot',
|
|
42
42
|
match: ['forgot', 'reset', 'quen-mat-khau', 'quên mật khẩu', 'doi-mat-khau'],
|
|
43
43
|
why: 'A customer who forgets a password has no way back in and writes to support instead. ' +
|
|
44
|
-
'
|
|
44
|
+
'sb_store action:"form" template:"forgot" with a page_name.',
|
|
45
45
|
},
|
|
46
46
|
{
|
|
47
47
|
key: 'contact',
|
|
48
48
|
match: ['contact', 'lien-he', 'liên hệ'],
|
|
49
49
|
why: 'No address, phone or form anywhere, so a shopper with a question about an order has ' +
|
|
50
|
-
'nowhere to put it.
|
|
50
|
+
'nowhere to put it. sb_store action:"form" template:"contact" with a page_name.',
|
|
51
51
|
},
|
|
52
52
|
{
|
|
53
53
|
key: 'about',
|
|
54
54
|
match: ['about', 'gioi-thieu', 'giới thiệu', 've-chung-toi'],
|
|
55
55
|
why: 'Nothing says who the shop is, which is the page a first-time buyer opens before paying.',
|
|
56
56
|
},
|
|
57
|
+
// TWO POLICIES, NOT ONE BUCKET. These were a single entry, and it read a shop
|
|
58
|
+
// carrying only "Chính sách giao hàng & đổi trả" as complete — which is
|
|
59
|
+
// exactly the shop that was measured, and it had no privacy terms at all. One
|
|
60
|
+
// policy page satisfying a check about all of them is the check answering a
|
|
61
|
+
// question it was not asked.
|
|
57
62
|
{
|
|
58
|
-
key: 'policy',
|
|
63
|
+
key: 'policy-delivery',
|
|
59
64
|
match: [
|
|
60
|
-
'
|
|
61
|
-
'
|
|
62
|
-
'shipping', 'van-chuyen', 'vận chuyển', 'giao-hang', 'giao hàng', 'refund',
|
|
65
|
+
'doi-tra', 'đổi trả', 'return', 'refund', 'hoan-tien', 'hoàn tiền',
|
|
66
|
+
'shipping', 'van-chuyen', 'vận chuyển', 'giao-hang', 'giao hàng', 'delivery',
|
|
63
67
|
],
|
|
64
|
-
why: 'No delivery
|
|
65
|
-
'
|
|
68
|
+
why: 'No delivery or return terms a shopper can read before paying. This is the page a buyer ' +
|
|
69
|
+
'looks for when the parcel is late and the one a dispute is settled against.',
|
|
70
|
+
},
|
|
71
|
+
{
|
|
72
|
+
key: 'policy-privacy',
|
|
73
|
+
match: [
|
|
74
|
+
'privacy', 'bao-mat', 'bảo mật', 'dieu-khoan', 'điều khoản', 'terms',
|
|
75
|
+
'quy-dinh', 'quy định',
|
|
76
|
+
],
|
|
77
|
+
why: 'No privacy policy or terms of use. Payment providers and marketplaces ask for both ' +
|
|
78
|
+
'before they will list a shop, and a checkout form collects personal data either way.',
|
|
79
|
+
},
|
|
80
|
+
{
|
|
81
|
+
key: 'faq',
|
|
82
|
+
match: ['faq', 'cau-hoi', 'câu hỏi', 'hoi-dap', 'hỏi đáp', 'help', 'tro-giup', 'trợ giúp'],
|
|
83
|
+
why: 'The same handful of questions reach support one message at a time, with no page to link ' +
|
|
84
|
+
'an answer to. The accordion element is what this page is built from.',
|
|
85
|
+
},
|
|
86
|
+
{
|
|
87
|
+
key: 'blog',
|
|
88
|
+
// ONLY ONCE THERE IS SOMETHING TO LIST. `post` is the ARTICLE template —
|
|
89
|
+
// it answers /blog/{slug} and has no address of its own — so a site with
|
|
90
|
+
// one can publish articles that nothing on the site links to. A site
|
|
91
|
+
// without one has no articles, and telling it to build a listing page is
|
|
92
|
+
// advice about a section it never asked for.
|
|
93
|
+
when: (pages) => pages.some((x) => x.type === 'post'),
|
|
94
|
+
match: ['blog', 'tin-tuc', 'tin tức', 'news', 'bai-viet', 'bài viết', 'kien-thuc', 'cẩm nang'],
|
|
95
|
+
why: 'This site has an article template, so /blog/{slug} works — but no page LISTS the ' +
|
|
96
|
+
'articles, so each one is reachable only by someone who already has its URL.',
|
|
66
97
|
},
|
|
67
98
|
];
|
|
68
99
|
/**
|
|
@@ -80,5 +111,5 @@ export function missingUsualPages(pages) {
|
|
|
80
111
|
.filter((p) => (typeof p.type === 'string' ? p.type === 'page' : true))
|
|
81
112
|
.map((p) => `${typeof p.slug === 'string' ? p.slug : ''} ${typeof p.name === 'string' ? p.name : ''}`.toLowerCase())
|
|
82
113
|
.join('\n');
|
|
83
|
-
return USUAL_PAGES.filter((u) => !u.match.some((m) => hay.includes(m)));
|
|
114
|
+
return USUAL_PAGES.filter((u) => (u.when ? u.when(pages) : true) && !u.match.some((m) => hay.includes(m)));
|
|
84
115
|
}
|
package/dist/tools/store.js
CHANGED
|
@@ -33,6 +33,7 @@ import { request, redact } from '../transport/http.js';
|
|
|
33
33
|
import { siteToken } from './credentialpick.js';
|
|
34
34
|
import { siteFor } from './context.js';
|
|
35
35
|
import { genId } from '../domains/site/ids.js';
|
|
36
|
+
import { addSubtree } from '../domains/site/builder.js';
|
|
36
37
|
import { chromeLinks, hasGlobal, shareChrome, sitePages } from './chrome.js';
|
|
37
38
|
import { tokensFromPage } from '../domains/site/importmap.js';
|
|
38
39
|
import { CHECKOUT_FORM, CHECKOUT_FORM_DOCUMENT, CHECKOUT_PAGE_DOCUMENT, CHECKOUT_TEXT, FORM_ID_SENTINEL, HEADLINE_SENTINEL, FORM_TEMPLATES, } from '../catalog/checkout.generated.js';
|
|
@@ -207,7 +208,54 @@ const FORM_TEMPLATE_KEYS = Object.keys(FORM_TEMPLATES).sort();
|
|
|
207
208
|
* returns — and `/account` is the one page that is not a free choice, because
|
|
208
209
|
* `membersOnlyRedirectTarget` sends every gated visitor there.
|
|
209
210
|
*/
|
|
210
|
-
|
|
211
|
+
/**
|
|
212
|
+
* The page a seeded form goes on, built and saved in the same call.
|
|
213
|
+
*
|
|
214
|
+
* WHY THIS EXISTS. seedForm made a form and stopped, and said so: "No page is
|
|
215
|
+
* made. Where a login form belongs is a design decision." True, and it left the
|
|
216
|
+
* caller three steps — create a page, sb_add a form element, sb_set its formId —
|
|
217
|
+
* with nothing insisting they belong together. Measured: a store built with
|
|
218
|
+
* these tools had no login page, no register page and no forgot page, and its
|
|
219
|
+
* own header had nothing to link to. The design decision was never the
|
|
220
|
+
* obstacle; the three steps were.
|
|
221
|
+
*
|
|
222
|
+
* A BLANK PAGE FIRST, THEN THE SUBTREE, rather than a document posted with the
|
|
223
|
+
* create. The checkout path can post one because it has a whole seeded document
|
|
224
|
+
* to post; here the document is one element, and going through the session is
|
|
225
|
+
* how every other builder in this server writes a page — band rules, id
|
|
226
|
+
* minting and the save contract all come with it instead of being re-derived.
|
|
227
|
+
*/
|
|
228
|
+
async function placeFormOnPage(ctx, session, siteId, formId, pageName, headline) {
|
|
229
|
+
const made = (await request({
|
|
230
|
+
base: ctx.base,
|
|
231
|
+
method: 'POST',
|
|
232
|
+
path: `/api/sites/${encodeURIComponent(siteId)}/pages`,
|
|
233
|
+
token: siteToken(ctx),
|
|
234
|
+
body: { name: pageName, type: 'page' },
|
|
235
|
+
fetchImpl: ctx.fetchImpl,
|
|
236
|
+
}));
|
|
237
|
+
const id = made.page?.id;
|
|
238
|
+
if (typeof id !== 'string' || !id) {
|
|
239
|
+
throw new Error('sbuilder: the platform accepted the page create and returned no page');
|
|
240
|
+
}
|
|
241
|
+
await session.open(siteId, id);
|
|
242
|
+
const doc = session.current();
|
|
243
|
+
const { patches } = addSubtree(doc, doc.doc.root_node_id, {
|
|
244
|
+
type: 'flex-section',
|
|
245
|
+
children: [
|
|
246
|
+
{
|
|
247
|
+
type: 'flex-block',
|
|
248
|
+
children: [
|
|
249
|
+
...(headline ? [{ type: 'heading', specials: { text: headline } }] : []),
|
|
250
|
+
{ type: 'form', specials: { formId } },
|
|
251
|
+
],
|
|
252
|
+
},
|
|
253
|
+
],
|
|
254
|
+
});
|
|
255
|
+
await session.applyAndSave(patches);
|
|
256
|
+
return { id, name: typeof made.page?.name === 'string' ? made.page.name : pageName };
|
|
257
|
+
}
|
|
258
|
+
async function seedForm(ctx, session, siteId, key, formName, pageName, headline, dryRun) {
|
|
211
259
|
const tpl = FORM_TEMPLATES[key];
|
|
212
260
|
const site = encodeURIComponent(siteId);
|
|
213
261
|
const name = formName ?? tpl.key;
|
|
@@ -236,6 +284,15 @@ async function seedForm(ctx, siteId, key, formName, dryRun) {
|
|
|
236
284
|
path: `/api/sites/${site}/forms/{formId}/document`,
|
|
237
285
|
},
|
|
238
286
|
],
|
|
287
|
+
...(pageName
|
|
288
|
+
? {
|
|
289
|
+
would_also: `create a page named ${JSON.stringify(pageName)} of type "page" and put ` +
|
|
290
|
+
'the form on it, so the form has an address a header can link to',
|
|
291
|
+
}
|
|
292
|
+
: {
|
|
293
|
+
no_page: 'Only the form. Pass page_name to have the page made and the form placed ' +
|
|
294
|
+
'on it in this same call — the three steps that otherwise get skipped.',
|
|
295
|
+
}),
|
|
239
296
|
preview: redact({ name, type: tpl.type }),
|
|
240
297
|
};
|
|
241
298
|
}
|
|
@@ -268,12 +325,34 @@ async function seedForm(ctx, siteId, key, formName, dryRun) {
|
|
|
268
325
|
await send('DELETE', `/api/sites/${site}/forms/${encodeURIComponent(form.id)}`).catch(() => undefined);
|
|
269
326
|
throw e;
|
|
270
327
|
}
|
|
328
|
+
// THE PAGE IS A SECOND WRITE AND MUST NOT UNDO THE FIRST. The form EXISTS the
|
|
329
|
+
// moment its three calls land; a refused page create leaves a form the caller
|
|
330
|
+
// can still place by hand, which is exactly what they had before this
|
|
331
|
+
// argument existed. Reporting the failure beats deleting a form they asked
|
|
332
|
+
// for — the same rule the seed follows in sb_page_create.
|
|
333
|
+
let page;
|
|
334
|
+
let page_failed;
|
|
335
|
+
if (pageName) {
|
|
336
|
+
try {
|
|
337
|
+
page = await placeFormOnPage(ctx, session, siteId, form.id, pageName, headline);
|
|
338
|
+
}
|
|
339
|
+
catch (e) {
|
|
340
|
+
page_failed = e.message.replace(/^sbuilder:\s*/, '').slice(0, 160);
|
|
341
|
+
}
|
|
342
|
+
}
|
|
271
343
|
return {
|
|
272
344
|
form: { id: form.id, type: form.type, name: form.name },
|
|
273
345
|
fields,
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
346
|
+
...(page ? { page } : {}),
|
|
347
|
+
...(page_failed ? { page_failed } : {}),
|
|
348
|
+
next: page
|
|
349
|
+
? `The form is on page ${page.id}. Publish it with sb_publish, and link to it from the ` +
|
|
350
|
+
'header. The submit button lives in the FORM DOCUMENT, not on the page, and a page ' +
|
|
351
|
+
'republish is what makes a form-document edit visible.'
|
|
352
|
+
: `Place it: sb_add a "form" element, then sb_set its specials.formId to "${form.id}" — ` +
|
|
353
|
+
'or pass page_name to have that page made for you. The submit button lives in the FORM ' +
|
|
354
|
+
'DOCUMENT, not on the page, and a page republish is what makes a form-document edit ' +
|
|
355
|
+
'visible.',
|
|
277
356
|
};
|
|
278
357
|
}
|
|
279
358
|
export function registerStoreTools(server, ctx, session) {
|
|
@@ -350,7 +429,7 @@ export function registerStoreTools(server, ctx, session) {
|
|
|
350
429
|
if (!template) {
|
|
351
430
|
throw new Error(`sbuilder: action:"form" needs a template. One of: ${FORM_TEMPLATE_KEYS.join(', ')}.`);
|
|
352
431
|
}
|
|
353
|
-
return text(await seedForm(ctx, siteId, template, name, dry_run !== false));
|
|
432
|
+
return text(await seedForm(ctx, session, siteId, template, name, page_name, headline, dry_run !== false));
|
|
354
433
|
}
|
|
355
434
|
const lang = (language ?? 'vi');
|
|
356
435
|
const t = CHECKOUT_TEXT[lang];
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sbuilder-mcp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.53.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",
|