sbuilder-mcp 0.81.0 → 0.81.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 +7 -0
- package/CHANGELOG.vi.md +7 -0
- package/dist/domains/site/builder.js +3 -1
- package/dist/domains/site/icons.js +59 -5
- package/dist/tools/live.js +8 -3
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -6,6 +6,13 @@ 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.81.1] - 2026-10-10
|
|
10
|
+
|
|
11
|
+
### Fixed
|
|
12
|
+
- `sb_set` and `sb_add` suggest the right icon for a misspelled name, ranking names that contain the typed word first, then close typos by edit distance, and honoring a `Fill` or `Line` suffix the caller wrote.
|
|
13
|
+
- `sb_duplicate` with `to_page_id` reports that `parent_id` is not on the target page and says to omit it or read ids with `sb_outline`.
|
|
14
|
+
- `sb_media_upload` of a font file tells the caller how to create a font family instead of suggesting `sb_set` with `src`.
|
|
15
|
+
|
|
9
16
|
## [0.81.0] - 2026-10-10
|
|
10
17
|
|
|
11
18
|
### Added
|
package/CHANGELOG.vi.md
CHANGED
|
@@ -6,6 +6,13 @@ 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.81.1] - 2026-10-10
|
|
10
|
+
|
|
11
|
+
### Fixed
|
|
12
|
+
- `sb_set` và `sb_add` gợi ý đúng icon cho tên gõ sai, ưu tiên tên chứa nguyên từ đã gõ, rồi đến lỗi gõ gần theo khoảng cách sửa, và tôn trọng đuôi `Fill` hoặc `Line` do người gọi viết.
|
|
13
|
+
- `sb_duplicate` với `to_page_id` báo `parent_id` không có trên trang đích và hướng dẫn bỏ qua nó hoặc đọc id bằng `sb_outline`.
|
|
14
|
+
- `sb_media_upload` với file font hướng dẫn cách tạo họ font thay vì gợi ý `sb_set` với `src`.
|
|
15
|
+
|
|
9
16
|
## [0.81.0] - 2026-10-10
|
|
10
17
|
|
|
11
18
|
### Added
|
|
@@ -564,7 +564,9 @@ export function copyNodeInto(src, id, target, parentId, index, guard) {
|
|
|
564
564
|
'again through its app.');
|
|
565
565
|
}
|
|
566
566
|
if (!parentId || !target.has(parentId)) {
|
|
567
|
-
throw new Error(
|
|
567
|
+
throw new Error(src === target
|
|
568
|
+
? `sbuilder: ${id} has no parent to be duplicated beside`
|
|
569
|
+
: `sbuilder: parent "${parentId}" is not on the target page — omit parent_id to paste at the page's end, or read its ids with sb_outline.`);
|
|
568
570
|
}
|
|
569
571
|
if (src !== target) {
|
|
570
572
|
// Onto ANOTHER document the parent is the caller's choice, so it gets the
|
|
@@ -27,10 +27,10 @@ let index = null;
|
|
|
27
27
|
* Nothing found → retried on the query's longest word, so "ShoppingBasketIcon"
|
|
28
28
|
* still lands near the basket.
|
|
29
29
|
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
30
|
+
* Substring ranking only — a SEARCH is a word someone chose. A misspelled NAME is
|
|
31
|
+
* `nearestIcons`' job, which tries edit distance first.
|
|
32
32
|
*/
|
|
33
|
-
export function searchIcons(query, cap = 30) {
|
|
33
|
+
export function searchIcons(query, cap = 30, retryOnWord = true) {
|
|
34
34
|
index ??= [...ICON_NAMES].map((n) => [n, stemOf(n)]);
|
|
35
35
|
const rank = (q) => !q
|
|
36
36
|
? []
|
|
@@ -40,7 +40,7 @@ export function searchIcons(query, cap = 30) {
|
|
|
40
40
|
.sort((a, b) => b.r - a.r || a.s.length - b.s.length || a.n.localeCompare(b.n))
|
|
41
41
|
.map((x) => x.n);
|
|
42
42
|
let hits = rank(stemOf(query));
|
|
43
|
-
if (!hits.length) {
|
|
43
|
+
if (!hits.length && retryOnWord) {
|
|
44
44
|
const words = query.replace(/([a-z])([A-Z])/g, '$1 $2').toLowerCase().split(/[^a-z0-9]+/);
|
|
45
45
|
const longest = words.filter((w) => w.length > 2).sort((a, b) => b.length - a.length)[0];
|
|
46
46
|
if (longest)
|
|
@@ -48,8 +48,62 @@ export function searchIcons(query, cap = 30) {
|
|
|
48
48
|
}
|
|
49
49
|
return hits.slice(0, cap);
|
|
50
50
|
}
|
|
51
|
+
/** Edit distance, capped: stops early past `max` (a name's stem is short). */
|
|
52
|
+
function distance(a, b, max) {
|
|
53
|
+
if (Math.abs(a.length - b.length) > max)
|
|
54
|
+
return max + 1;
|
|
55
|
+
let prev = Array.from({ length: b.length + 1 }, (_, j) => j);
|
|
56
|
+
for (let i = 1; i <= a.length; i++) {
|
|
57
|
+
const cur = [i];
|
|
58
|
+
let best = i;
|
|
59
|
+
for (let j = 1; j <= b.length; j++) {
|
|
60
|
+
cur[j] = Math.min(prev[j] + 1, cur[j - 1] + 1, prev[j - 1] + (a[i - 1] === b[j - 1] ? 0 : 1));
|
|
61
|
+
best = Math.min(best, cur[j]);
|
|
62
|
+
}
|
|
63
|
+
if (best > max)
|
|
64
|
+
return max + 1;
|
|
65
|
+
prev = cur;
|
|
66
|
+
}
|
|
67
|
+
return prev[b.length];
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* The names a WRONG value most likely meant. A typo first ("ShoppingCartt" is
|
|
71
|
+
* one edit from ShoppingCart, while a word search on "shopping" ranks the shorter
|
|
72
|
+
* ShoppingBag ahead of it — measured on a live platform), then the search.
|
|
73
|
+
* Fill before Line on a tie: the platform's own default (StarFill) is a Fill.
|
|
74
|
+
*/
|
|
51
75
|
export function nearestIcons(query) {
|
|
52
|
-
|
|
76
|
+
index ??= [...ICON_NAMES].map((n) => [n, stemOf(n)]);
|
|
77
|
+
const q = stemOf(query);
|
|
78
|
+
const max = Math.max(1, Math.floor(q.length / 4));
|
|
79
|
+
// The suffix the caller wrote wins a tie; otherwise Fill, the default's own.
|
|
80
|
+
const want = /line$/i.test(norm(query)) ? 'Line' : 'Fill';
|
|
81
|
+
const typos = q
|
|
82
|
+
? index
|
|
83
|
+
.map(([n, s]) => ({ n, s, d: distance(q, s, max) }))
|
|
84
|
+
.filter((x) => x.d <= max)
|
|
85
|
+
.sort((a, b) => a.d - b.d ||
|
|
86
|
+
// ShoppingCart over ShoppingCart2 for "ShoppingCartt": a stem the query
|
|
87
|
+
// extends is a closer guess than a sibling the same distance away.
|
|
88
|
+
Number(q.startsWith(b.s) || b.s.startsWith(q)) - Number(q.startsWith(a.s) || a.s.startsWith(q)) ||
|
|
89
|
+
Number(b.n.endsWith(want)) - Number(a.n.endsWith(want)) ||
|
|
90
|
+
a.s.length - b.s.length ||
|
|
91
|
+
a.n.localeCompare(b.n))
|
|
92
|
+
.map((x) => x.n)
|
|
93
|
+
: [];
|
|
94
|
+
// A value that is a WHOLE WORD of real names ("cart" in Shopping·Cart, "user")
|
|
95
|
+
// means those names, not Car/Cast one edit away; a fragment inside another word
|
|
96
|
+
// ("hart" in c·hart) is more likely a typo (Heart). So: word hits, then typos,
|
|
97
|
+
// then whatever else the search finds.
|
|
98
|
+
const wordsOf = (n) => n.replace(/([a-z0-9])([A-Z])/g, '$1 $2').toLowerCase().split(' ');
|
|
99
|
+
const words = q
|
|
100
|
+
? index
|
|
101
|
+
.filter(([n, st]) => st.startsWith(q) || wordsOf(n).includes(q))
|
|
102
|
+
.sort(([an, as], [bn, bs]) => as.length - bs.length || Number(bn.endsWith(want)) - Number(an.endsWith(want)) || an.localeCompare(bn))
|
|
103
|
+
.map(([n]) => n)
|
|
104
|
+
: [];
|
|
105
|
+
const order = [...words, ...typos, ...searchIcons(query, 5)];
|
|
106
|
+
return [...new Set(order)].slice(0, 5);
|
|
53
107
|
}
|
|
54
108
|
/**
|
|
55
109
|
* The problem sentence for `value` written to `<ns>.<key>` on `type`, or null
|
package/dist/tools/live.js
CHANGED
|
@@ -652,11 +652,16 @@ export function registerLiveTools(server, ctx, session) {
|
|
|
652
652
|
});
|
|
653
653
|
}
|
|
654
654
|
const asset = await uploadMedia(ctx, site_id, { path, url, name, folderId: folder_id });
|
|
655
|
+
const font = asset.mediaType === 'font';
|
|
655
656
|
return text({
|
|
656
657
|
asset,
|
|
657
|
-
next: asset.url
|
|
658
|
-
?
|
|
659
|
-
:
|
|
658
|
+
next: !asset.url
|
|
659
|
+
? 'Uploaded, but the server returned no url — read it back with sb_media_list.'
|
|
660
|
+
: font
|
|
661
|
+
? `A font: give it a family — POST /api/sites/{siteId}/fonts {name, source:"upload"}, then PUT ` +
|
|
662
|
+
`…/fonts/{groupId}/files {weight, url: ${JSON.stringify(asset.url)}, format, sizeBytes}, ` +
|
|
663
|
+
'and use the family by NAME (settings fontFamily or a theme text style).'
|
|
664
|
+
: `Use it: sb_set id "<node>", namespace specials, keys { "src": ${JSON.stringify(asset.url)} }`,
|
|
660
665
|
});
|
|
661
666
|
});
|
|
662
667
|
server.registerTool('sb_event', {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "sbuilder-mcp",
|
|
3
|
-
"version": "0.81.
|
|
3
|
+
"version": "0.81.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",
|