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 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(`sbuilder: ${id} has no parent to be duplicated beside`);
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
- * ponytail: substring ranking, no edit distance — a misspelling ("hart") finds
31
- * nothing. Add a Levenshtein fallback if callers misspell rather than guess.
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
- return searchIcons(query, 5);
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
@@ -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
- ? `Use it: sb_set id "<node>", namespace specials, keys { "src": ${JSON.stringify(asset.url)} }`
659
- : 'Uploaded, but the server returned no url — read it back with sb_media_list.',
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.0",
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",